use std::sync::LazyLock;
use ahash::AHashMap;
use databento::dbn;
use jiff::{
civil::Time,
tz::{AmbiguousOffset, Offset, TimeZone},
};
use nautilus_core::{
UnixNanos,
datetime::{NANOSECONDS_IN_DAY, get_timezone},
};
use ustr::Ustr;
static DEFAULT_CONFIG: LazyLock<DatabentoDecodeConfig> =
LazyLock::new(DatabentoDecodeConfig::default);
static NEW_YORK: LazyLock<TimeZone> =
LazyLock::new(|| get_timezone("America/New_York").expect("bundled America/New_York timezone"));
const fn opra_default_time() -> Time {
Time::constant(16, 0, 0, 0)
}
#[derive(Clone, Debug)]
pub struct OptionExpirationRule {
pub timezone: TimeZone,
pub default_time: Time,
pub overrides: AHashMap<Ustr, Time>,
}
impl OptionExpirationRule {
#[must_use]
pub fn opra() -> Self {
Self {
timezone: NEW_YORK.clone(),
default_time: opra_default_time(),
overrides: AHashMap::new(),
}
}
fn time_for(&self, underlying: Ustr) -> Time {
self.overrides
.get(&underlying)
.copied()
.unwrap_or(self.default_time)
}
}
#[derive(Clone, Debug)]
pub struct DatabentoDecodeConfig {
pub option_expiration: AHashMap<dbn::Dataset, OptionExpirationRule>,
}
impl Default for DatabentoDecodeConfig {
fn default() -> Self {
let mut option_expiration = AHashMap::new();
option_expiration.insert(dbn::Dataset::OpraPillar, OptionExpirationRule::opra());
Self { option_expiration }
}
}
#[must_use]
pub fn corrected_option_expiration(
expiration: UnixNanos,
underlying: Ustr,
dataset: Option<dbn::Dataset>,
config: Option<&DatabentoDecodeConfig>,
) -> UnixNanos {
let Some(dataset) = dataset else {
return expiration;
};
let config = config.unwrap_or(&DEFAULT_CONFIG);
let Some(rule) = config.option_expiration.get(&dataset) else {
return expiration;
};
let raw = expiration.as_u64();
if raw == 0 || !raw.is_multiple_of(NANOSECONDS_IN_DAY) {
return expiration;
}
let date = Offset::UTC.to_datetime(expiration.to_datetime_utc()).date();
let ambiguous = rule
.timezone
.to_ambiguous_timestamp(date.to_datetime(rule.time_for(underlying)));
let corrected = match ambiguous.offset() {
AmbiguousOffset::Unambiguous { .. } => ambiguous.unambiguous(),
AmbiguousOffset::Fold { .. } => ambiguous.earlier(),
AmbiguousOffset::Gap { .. } => return expiration,
};
corrected
.ok()
.and_then(|timestamp| u64::try_from(timestamp.as_nanosecond()).ok())
.map_or(expiration, UnixNanos::from)
}
#[cfg(test)]
mod tests {
use databento::dbn;
use jiff::civil::Time;
use nautilus_core::UnixNanos;
use rstest::rstest;
use ustr::Ustr;
use super::{DatabentoDecodeConfig, corrected_option_expiration};
const EDT_MIDNIGHT_UTC: u64 = 1_782_691_200_000_000_000; const EDT_1600_ET: u64 = 1_782_763_200_000_000_000; const EST_MIDNIGHT_UTC: u64 = 1_768_521_600_000_000_000; const EST_1600_ET: u64 = 1_768_597_200_000_000_000; const EDT_0930_ET: u64 = 1_782_739_800_000_000_000; const INTRADAY_UTC: u64 = 1_789_738_200_000_000_000;
fn config_with_opra_override(underlying: &str, time: Time) -> DatabentoDecodeConfig {
let mut config = DatabentoDecodeConfig::default();
config
.option_expiration
.get_mut(&dbn::Dataset::OpraPillar)
.unwrap()
.overrides
.insert(Ustr::from(underlying), time);
config
}
#[rstest]
fn test_opra_midnight_corrected_to_1600_et_during_edt() {
let result = corrected_option_expiration(
UnixNanos::from(EDT_MIDNIGHT_UTC),
Ustr::from("SPX"),
Some(dbn::Dataset::OpraPillar),
None,
);
assert_eq!(result.as_u64(), EDT_1600_ET);
}
#[rstest]
fn test_opra_midnight_corrected_to_1600_et_during_est() {
let result = corrected_option_expiration(
UnixNanos::from(EST_MIDNIGHT_UTC),
Ustr::from("SPX"),
Some(dbn::Dataset::OpraPillar),
None,
);
assert_eq!(result.as_u64(), EST_1600_ET);
}
#[rstest]
fn test_opra_override_applied_for_matching_underlying() {
let config = config_with_opra_override("XSP", Time::constant(9, 30, 0, 0));
let result = corrected_option_expiration(
UnixNanos::from(EDT_MIDNIGHT_UTC),
Ustr::from("XSP"),
Some(dbn::Dataset::OpraPillar),
Some(&config),
);
assert_eq!(result.as_u64(), EDT_0930_ET);
}
#[rstest]
fn test_opra_default_used_when_underlying_not_overridden() {
let config = config_with_opra_override("XSP", Time::constant(9, 30, 0, 0));
let result = corrected_option_expiration(
UnixNanos::from(EDT_MIDNIGHT_UTC),
Ustr::from("SPX"),
Some(dbn::Dataset::OpraPillar),
Some(&config),
);
assert_eq!(result.as_u64(), EDT_1600_ET);
}
#[rstest]
fn test_opra_intraday_expiration_passes_through() {
let result = corrected_option_expiration(
UnixNanos::from(INTRADAY_UTC),
Ustr::from("SPX"),
Some(dbn::Dataset::OpraPillar),
None,
);
assert_eq!(result.as_u64(), INTRADAY_UTC);
}
#[rstest]
fn test_non_opra_midnight_passes_through() {
let result = corrected_option_expiration(
UnixNanos::from(EDT_MIDNIGHT_UTC),
Ustr::from("ESU6"),
Some(dbn::Dataset::GlbxMdp3),
None,
);
assert_eq!(result.as_u64(), EDT_MIDNIGHT_UTC);
}
#[rstest]
fn test_unknown_dataset_passes_through() {
let result = corrected_option_expiration(
UnixNanos::from(EDT_MIDNIGHT_UTC),
Ustr::from("SPX"),
None,
None,
);
assert_eq!(result.as_u64(), EDT_MIDNIGHT_UTC);
}
#[rstest]
fn test_dataset_without_rule_passes_through() {
let config = DatabentoDecodeConfig {
option_expiration: ahash::AHashMap::new(),
};
let result = corrected_option_expiration(
UnixNanos::from(EDT_MIDNIGHT_UTC),
Ustr::from("SPX"),
Some(dbn::Dataset::OpraPillar),
Some(&config),
);
assert_eq!(result.as_u64(), EDT_MIDNIGHT_UTC);
}
}