test(aura-ingest): pin GER40 archive is genuine UTC + session DST-correct

data-server does no timezone math (delphi_to_unix_ms is pure arithmetic), so the
archive carries whatever the raw Pepperstone files contain, labelled as UTC. The
Session node's UTC->Berlin DST conversion is unit-tested, but the assumption that
the raw timestamps are themselves UTC was unverified — a broker-local archive
would shift the whole session and the error would jump across the DST boundary.

Verified empirically via the DAX cash-open volume spike (GER40): it lands at
Berlin 09:00 in BOTH seasons, at 07:00 UTC (summer, CEST, UTC+2) and 08:00 UTC
(winter, CET, UTC+1) — a clean +1h DST shift, the genuine-UTC signature. This
gated test pins it so a future archive/convention drift cannot silently corrupt
every session strategy's bar alignment.
This commit is contained in:
2026-06-17 21:44:48 +02:00
parent dcd550788d
commit dee25afcd5
@@ -0,0 +1,82 @@
//! Gated integration test: the GER40 archive timestamps are **genuine UTC**, so
//! the Frankfurt session aligns **DST-correctly end-to-end** (raw archive →
//! `unix_ms_to_epoch_ns` → the `Session` node's Berlin conversion).
//!
//! Why this needs pinning: `data-server` applies **no timezone math** — its
//! `delphi_to_unix_ms` is pure arithmetic (Delphi `TDateTime` → Unix-ms, no
//! offset, no DST), so the archive carries whatever the raw Pepperstone files
//! contain, *labelled* as Unix-ms-UTC. The `Session` node's UTC→Berlin DST math
//! is unit-tested in isolation, but the assumption that the raw timestamps are
//! UTC was otherwise unverified — a broker-local (e.g. EET) archive would shift
//! the whole session and the error would jump across the DST boundary.
//!
//! Decisive market signal (the DAX cash open at 09:00 Frankfurt is the morning's
//! sharp **volume** spike): in genuine-UTC data that spike renders at **Berlin
//! 09:00 in BOTH seasons**, and the same instant is **07:00 UTC** (summer, CEST,
//! UTC+2) / **08:00 UTC** (winter, CET, UTC+1) — a clean +1h DST shift. A
//! fixed-offset / broker-local archive would put the spike at the wrong Berlin
//! hour or fail to shift across DST.
//!
//! Mirrors `real_bars.rs`: skips with a note where the local Pepperstone archive
//! is absent, so `cargo test --workspace` stays green anywhere.
use std::sync::Arc;
use aura_ingest::load_m1_window;
use chrono::{TimeZone, Timelike};
use chrono_tz::Europe::Berlin;
use data_server::{DataServer, DEFAULT_DATA_PATH};
/// For one UTC calendar day of GER40, return `(morning-peak Berlin hour over
/// 06..=11, the UTC hour that the Berlin-09:00 hour maps to)`. The morning volume
/// peak is the DAX cash open; in genuine-UTC data it sits at Berlin 09:00 and the
/// same instant is 07:00 UTC (CEST) / 08:00 UTC (CET).
fn morning_open(server: &Arc<DataServer>, y: i32, m: u32, d: u32) -> (u32, u32) {
let from = chrono::Utc.with_ymd_and_hms(y, m, d, 0, 0, 0).unwrap().timestamp_millis();
let to = from + 24 * 3600 * 1000 - 1;
let cols = load_m1_window(server, "GER40", Some(from), Some(to))
.expect("GER40 has data on this day");
let mut vol_by_berlin_hour = [0i64; 24];
let mut utc_hour_of_berlin_9: Option<u32> = None;
for i in 0..cols.ts.len() {
let ns = cols.ts[i].0;
let berlin_hour = Berlin.timestamp_nanos(ns).hour();
vol_by_berlin_hour[berlin_hour as usize] += cols.volume[i];
if berlin_hour == 9 && utc_hour_of_berlin_9.is_none() {
utc_hour_of_berlin_9 = Some(chrono::Utc.timestamp_nanos(ns).hour());
}
}
let morning_peak = (6u32..=11)
.max_by_key(|&h| vol_by_berlin_hour[h as usize])
.unwrap();
(morning_peak, utc_hour_of_berlin_9.expect("bars exist in the Berlin 09:00 hour"))
}
#[test]
fn ger40_archive_is_utc_and_session_is_dst_correct() {
let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));
if !server.has_symbol("GER40") {
eprintln!("skip: no local data at {DEFAULT_DATA_PATH} (symbol GER40 absent)");
return; // hermetic elsewhere; exercises the real archive where files exist
}
// Two normal trading Mondays, one each side of the EU DST boundary.
let (summer_peak, summer_utc) = morning_open(&server, 2024, 7, 15);
let (winter_peak, winter_utc) = morning_open(&server, 2024, 1, 15);
// The morning volume peak is the DAX cash open, at Berlin 09:00 in BOTH
// seasons — the data, rendered local, places the open at the right wall-clock.
assert_eq!(summer_peak, 9, "summer: morning volume must peak at the Berlin 09:00 cash open");
assert_eq!(winter_peak, 9, "winter: morning volume must peak at the Berlin 09:00 cash open");
// ...and Berlin 09:00 is the DST-correct UTC instant: 07:00 (CEST, UTC+2) in
// summer, 08:00 (CET, UTC+1) in winter. The shift is the genuine-UTC
// signature (a fixed-offset / broker-local archive would not shift).
assert_eq!(summer_utc, 7, "summer: Berlin 09:00 must be 07:00 UTC (CEST, UTC+2)");
assert_eq!(winter_utc, 8, "winter: Berlin 09:00 must be 08:00 UTC (CET, UTC+1)");
assert_ne!(
summer_utc, winter_utc,
"the open's UTC instant must shift across DST — proof the archive is genuine UTC",
);
}