Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [8.0.34] - 2026-05-07

### Changed

- **`crates/thetadatadx/src/decode.rs` (2177 LoC) split into
`mdds/decode/{error,headers,transport,extract,cell,v3}` modules.**
Pure structural refactor; public API unchanged. Re-exports
preserved at `thetadatadx::mdds::decode::*`.

- **Eastern-time + DST primitives lifted to `tdbe::time`.**
`eastern_offset_ms`, `march_second_sunday_utc`,
`november_first_sunday_utc`, `april_first_sunday_utc`,
`october_last_sunday_utc`, `civil_to_epoch_days`,
`timestamp_to_ms_of_day`, `timestamp_to_date` — single canonical
module reused by mdds, fpss, flatfiles. Patch bump tdbe 0.12.9
→ 0.12.10.

Refs #500.

## [8.0.33] - 2026-05-07

### Added
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/tdbe/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "tdbe"
version = "0.12.9"
version = "0.12.10"
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
Expand Down
88 changes: 4 additions & 84 deletions crates/tdbe/src/latency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
//! (YYYYMMDD) into epoch nanoseconds, then subtracts from the local
//! `received_at_ns` wall-clock timestamp captured at frame decode time.
//!
//! No external timezone crate -- uses the same civil-date math and US DST
//! rules (Energy Policy Act 2005) as `thetadatadx::decode`.
//! Civil-date / DST primitives live in [`crate::time`]; this module is a
//! thin wrapper that adds the YYYYMMDD-and-`ms_of_day` decomposition.

use crate::time::{civil_to_epoch_days, eastern_offset_ms};

/// Compute wire-to-application latency in nanoseconds.
///
Expand Down Expand Up @@ -52,88 +54,6 @@ fn exchange_epoch_ns(ms_of_day: i32, date_yyyymmdd: i32) -> i64 {
exchange_epoch_ms * 1_000_000
}

// ---------------------------------------------------------------------------
// Civil-date / DST helpers (same algorithm as thetadatadx::decode)
// ---------------------------------------------------------------------------

/// Convert civil date to days since 1970-01-01 (Euclidean algorithm).
// Reason: the Euclidean date algorithm uses intentional signed/unsigned conversions
// that are safe for all valid calendar dates (year 0..9999).
#[allow(clippy::cast_sign_loss, clippy::cast_possible_wrap)]
fn civil_to_epoch_days(year: i32, month: u32, day: u32) -> i64 {
let y = if month <= 2 {
i64::from(year) - 1
} else {
i64::from(year)
};
let m = if month <= 2 {
i64::from(month) + 9
} else {
i64::from(month) - 3
};
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as u64;
let doy = (153 * m as u64 + 2) / 5 + u64::from(day) - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe as i64 - 719_468
}

/// Eastern Time UTC offset in milliseconds for a given `epoch_ms`.
///
/// US DST rule (Energy Policy Act of 2005):
/// - EDT (UTC-4): second Sunday of March 2:00 AM local -> first Sunday of November 2:00 AM local
/// - EST (UTC-5): rest of the year
// Reason: the Euclidean date algorithm uses intentional signed/unsigned conversions
// that are safe for all valid epoch timestamps in the market data date range.
#[allow(
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
fn eastern_offset_ms(epoch_ms: u64) -> i64 {
let epoch_secs = epoch_ms as i64 / 1_000;
let days_since_epoch = epoch_secs / 86_400;

// Civil date from days since 1970-01-01.
let z = days_since_epoch + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u32;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let year = yoe as i32 + (era * 400) as i32;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if month <= 2 { year + 1 } else { year };

let dst_start_utc = march_second_sunday_utc(year);
let dst_end_utc = november_first_sunday_utc(year);

let epoch_ms_i64 = epoch_ms as i64;
if epoch_ms_i64 >= dst_start_utc && epoch_ms_i64 < dst_end_utc {
-4 * 3_600 * 1_000 // EDT
} else {
-5 * 3_600 * 1_000 // EST
}
}

/// Epoch ms of the second Sunday of March at 7:00 AM UTC (= 2:00 AM EST).
fn march_second_sunday_utc(year: i32) -> i64 {
let mar1 = civil_to_epoch_days(year, 3, 1);
let dow = ((mar1 + 3) % 7 + 7) % 7; // 0=Mon..6=Sun
let days_to_first_sunday = (6 - dow + 7) % 7;
let second_sunday = mar1 + days_to_first_sunday + 7;
second_sunday * 86_400_000 + 7 * 3_600 * 1_000
}

/// Epoch ms of the first Sunday of November at 6:00 AM UTC (= 2:00 AM EDT).
fn november_first_sunday_utc(year: i32) -> i64 {
let nov1 = civil_to_epoch_days(year, 11, 1);
let dow = ((nov1 + 3) % 7 + 7) % 7;
let days_to_first_sunday = (6 - dow + 7) % 7;
let first_sunday = nov1 + days_to_first_sunday;
first_sunday * 86_400_000 + 6 * 3_600 * 1_000
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
1 change: 1 addition & 0 deletions crates/tdbe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub mod json_canon;
pub mod latency;
pub mod right;
pub mod sequences;
pub mod time;
pub mod types;

// Convenience re-exports at crate root
Expand Down
Loading
Loading