use std::str::FromStr;
use nautilus_core::UnixNanos;
pub use nautilus_core::serialization::{
deserialize_decimal_from_str, deserialize_decimal_or_zero,
deserialize_optional_decimal_from_str, deserialize_string_to_u64, serialize_decimal_as_str,
serialize_optional_decimal_as_str,
};
use nautilus_model::{
data::BarType,
enums::{AggregationSource, BarAggregation},
};
use serde::{
Deserialize,
de::{self, Unexpected},
};
use crate::common::enums::{CoinbaseGranularity, CoinbaseMarginType, CoinbaseProductType};
pub fn deserialize_empty_string_to_none<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum EmptyOrValue<T> {
Value(T),
Empty(String),
}
match Option::<EmptyOrValue<T>>::deserialize(deserializer)? {
None => Ok(None),
Some(EmptyOrValue::Value(value)) => Ok(Some(value)),
Some(EmptyOrValue::Empty(value)) if value.is_empty() => Ok(None),
Some(EmptyOrValue::Empty(value)) => Err(de::Error::invalid_value(
Unexpected::Str(&value),
&"an empty string or a valid value",
)),
}
}
pub fn deserialize_product_type_or_unknown<'de, D>(
deserializer: D,
) -> Result<CoinbaseProductType, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(CoinbaseProductType::from_str(&value).unwrap_or(CoinbaseProductType::Unknown))
}
pub fn deserialize_margin_type_or_none<'de, D>(
deserializer: D,
) -> Result<Option<CoinbaseMarginType>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<String>::deserialize(deserializer)?;
Ok(value
.filter(|s| !s.is_empty())
.and_then(|s| CoinbaseMarginType::from_str(&s).ok()))
}
pub fn format_rfc3339_from_nanos(ts: UnixNanos) -> anyhow::Result<String> {
let secs = (ts.as_u64() / 1_000_000_000) as i64;
let nanos = (ts.as_u64() % 1_000_000_000) as u32;
chrono::DateTime::<chrono::Utc>::from_timestamp(secs, nanos)
.map(|dt| dt.to_rfc3339())
.ok_or_else(|| anyhow::anyhow!("UnixNanos {ts} is out of range for chrono::DateTime"))
}
pub fn bar_type_to_granularity(bar_type: &BarType) -> anyhow::Result<CoinbaseGranularity> {
let spec = bar_type.spec();
anyhow::ensure!(
bar_type.aggregation_source() == AggregationSource::External,
"Only EXTERNAL aggregation is supported"
);
let step = spec.step.get();
match spec.aggregation {
BarAggregation::Minute => match step {
1 => Ok(CoinbaseGranularity::OneMinute),
5 => Ok(CoinbaseGranularity::FiveMinute),
15 => Ok(CoinbaseGranularity::FifteenMinute),
30 => Ok(CoinbaseGranularity::ThirtyMinute),
_ => anyhow::bail!("Unsupported minute step: {step}"),
},
BarAggregation::Hour => match step {
1 => Ok(CoinbaseGranularity::OneHour),
2 => Ok(CoinbaseGranularity::TwoHour),
6 => Ok(CoinbaseGranularity::SixHour),
_ => anyhow::bail!("Unsupported hour step: {step}"),
},
BarAggregation::Day => match step {
1 => Ok(CoinbaseGranularity::OneDay),
_ => anyhow::bail!("Unsupported day step: {step}"),
},
other => anyhow::bail!("Unsupported aggregation: {other}"),
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case(
"BTC-USD.COINBASE-1-MINUTE-LAST-EXTERNAL",
CoinbaseGranularity::OneMinute
)]
#[case(
"BTC-USD.COINBASE-5-MINUTE-LAST-EXTERNAL",
CoinbaseGranularity::FiveMinute
)]
#[case(
"BTC-USD.COINBASE-15-MINUTE-LAST-EXTERNAL",
CoinbaseGranularity::FifteenMinute
)]
#[case(
"BTC-USD.COINBASE-30-MINUTE-LAST-EXTERNAL",
CoinbaseGranularity::ThirtyMinute
)]
#[case("BTC-USD.COINBASE-1-HOUR-LAST-EXTERNAL", CoinbaseGranularity::OneHour)]
#[case("BTC-USD.COINBASE-2-HOUR-LAST-EXTERNAL", CoinbaseGranularity::TwoHour)]
#[case("BTC-USD.COINBASE-6-HOUR-LAST-EXTERNAL", CoinbaseGranularity::SixHour)]
#[case("BTC-USD.COINBASE-1-DAY-LAST-EXTERNAL", CoinbaseGranularity::OneDay)]
fn test_bar_type_to_granularity(
#[case] bar_type_str: &str,
#[case] expected: CoinbaseGranularity,
) {
let bar_type = BarType::from(bar_type_str);
let result = bar_type_to_granularity(&bar_type).unwrap();
assert_eq!(result, expected);
}
#[rstest]
#[case("BTC-USD.COINBASE-3-MINUTE-LAST-EXTERNAL")]
#[case("BTC-USD.COINBASE-4-HOUR-LAST-EXTERNAL")]
#[case("BTC-USD.COINBASE-2-DAY-LAST-EXTERNAL")]
fn test_bar_type_to_granularity_unsupported(#[case] bar_type_str: &str) {
let bar_type = BarType::from(bar_type_str);
assert!(bar_type_to_granularity(&bar_type).is_err());
}
#[rstest]
fn test_format_rfc3339_from_nanos_round_trip() {
let ts = UnixNanos::from(1_705_314_600_000_000_000u64);
let s = format_rfc3339_from_nanos(ts).unwrap();
assert_eq!(s, "2024-01-15T10:30:00+00:00");
}
#[rstest]
fn test_format_rfc3339_from_nanos_preserves_subsecond_precision() {
let ts = UnixNanos::from(1_705_314_600_123_456_789u64);
let s = format_rfc3339_from_nanos(ts).unwrap();
assert_eq!(s, "2024-01-15T10:30:00.123456789+00:00");
}
}