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:
@@ -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",
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user