8c9a1b4630
A real OHLC source now builds from aura-ingest alone, in the engine-native epoch-ns Timestamp currency, with the one ms<->ns crossing owned by the seam. Library surface (src/lib.rs), all additive — the ms-based `open` and `unix_ms_to_epoch_ns` are untouched, so every existing call site is preserved byte-for-byte: - epoch_ns_to_unix_ms (private): the seam-owned inverse; consumers never convert (#80). - M1FieldSource::open_window: the Timestamp-window mirror of `open`, mapping each bound through the private inverse and delegating to `open`. - open_ohlc: the canonical OHLC bundle opener — four M1FieldSources in the fixed open/high/low/close C4 merge order, sharing one Arc<DataServer> (one cache, C12); the order lives in one vetted place (#92). - default_data_server + `pub use data_server::{DataServer, DEFAULT_DATA_PATH}`: a real-data source builds without naming the external data_server crate (#81). Consumer migration: the shared breakout_real.rs helpers and every GER40 example/test move to the Timestamp-native surface — open_ohlc_sources removed, utc_month_window_ms -> utc_month_window (Timestamp), report_from_trace retyped, the per-consumer ns_to_ms hand-divides deleted, the direct data_server imports replaced by the re-exports. ger40_breakout_compare.rs (which opened OHLC by its own hand-loop, reached by neither retyped helper) is migrated by inspection. Return type is Vec, not [_; 4]: Harness::run consumes Vec<Box<dyn Source>>, so it feeds straight in; order-safety comes from the single helper either way. Tests: a hermetic round-trip pinning the inverse; a gated behaviour-preservation test (open_ohlc Timestamp path == ms-path open, bit-identical recorded series); a hermetic absent-archive fixture pinning the file-level None contract and the #81 re-export firewall. Verified: cargo build --workspace --all-targets clean; cargo test --workspace green (the gated GER40 tests ran against the local archive — open_ohlc_seam proves byte-identity on real data, not a skip); clippy --all-targets -D warnings clean. Acceptance greps pass: open_ohlc_sources gone, no consumer-side ns_to_ms, no data_server import in any migrated GER40 consumer, epoch_ns_to_unix_ms private. closes #80, #81, #92
72 lines
3.1 KiB
Rust
72 lines
3.1 KiB
Rust
//! Gated behaviour-preservation proof for the consolidated OHLC opener (#80/#92):
|
||
//! the library `open_ohlc` over an epoch-ns `Timestamp` window produces a recorded
|
||
//! `(held, equity)` series BIT-IDENTICAL to opening the four `M1FieldSource`s by
|
||
//! hand over the equivalent Unix-ms window via the untouched ms-path `open`. Same
|
||
//! instants, same bars, same numbers — the migration changes the surface, not the
|
||
//! behaviour.
|
||
//!
|
||
//! Mirrors the other gated GER40 tests: skips with a note where the local
|
||
//! Pepperstone archive is absent, so `cargo test --workspace` stays green
|
||
//! anywhere; exercises the real path where the files exist.
|
||
|
||
use std::sync::Arc;
|
||
|
||
use aura_engine::{Harness, Source};
|
||
use chrono::TimeZone;
|
||
use data_server::{DataServer, DEFAULT_DATA_PATH};
|
||
|
||
use aura_ingest::{open_ohlc, M1Field, M1FieldSource};
|
||
|
||
#[path = "../examples/shared/breakout_real.rs"]
|
||
mod breakout_real;
|
||
use breakout_real::*;
|
||
|
||
/// Run a fresh harness over `sources` and drain the recorded `(held, equity)`
|
||
/// series (the same fold the determinism tests use).
|
||
fn series_from(mut h: Harness, taps: &Taps, sources: Vec<Box<dyn Source>>) -> Vec<(f64, f64)> {
|
||
h.run(sources);
|
||
drop(h);
|
||
let trace = drain_trace(taps);
|
||
trace.iter().map(|b| (b.held, b.equity)).collect()
|
||
}
|
||
|
||
/// Property: the Timestamp `open_ohlc` path and the hand-rolled ms-path `open`
|
||
/// (×4, in O/H/L/C order) over the equivalent window yield bit-identical recorded
|
||
/// series — behaviour-preserving consolidation (AC5).
|
||
#[test]
|
||
fn open_ohlc_timestamp_path_matches_ms_path_open() {
|
||
let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));
|
||
if !server.has_symbol(SYMBOL) {
|
||
eprintln!("skip: no local data at {DEFAULT_DATA_PATH} (symbol {SYMBOL} absent)");
|
||
return;
|
||
}
|
||
|
||
// September 2024 (inclusive UTC), the window the other gated tests pin.
|
||
let from_ms = chrono::Utc.with_ymd_and_hms(2024, 9, 1, 0, 0, 0).unwrap().timestamp_millis();
|
||
let to_ms = chrono::Utc.with_ymd_and_hms(2024, 9, 30, 23, 59, 59).unwrap().timestamp_millis();
|
||
let from_ts = aura_ingest::unix_ms_to_epoch_ns(from_ms);
|
||
let to_ts = aura_ingest::unix_ms_to_epoch_ns(to_ms);
|
||
|
||
// Path A: the consolidated Timestamp opener.
|
||
let (h_a, taps_a) = build_harness();
|
||
let sources_a = open_ohlc(&server, SYMBOL, from_ts, to_ts).expect("window overlaps GER40 data");
|
||
let series_a = series_from(h_a, &taps_a, sources_a);
|
||
assert!(!series_a.is_empty(), "window resolved to zero bars");
|
||
|
||
// Path B: the untouched ms-path open, four fields by hand in O/H/L/C order.
|
||
let fields = [M1Field::Open, M1Field::High, M1Field::Low, M1Field::Close];
|
||
let mut sources_b: Vec<Box<dyn Source>> = Vec::with_capacity(4);
|
||
for &f in &fields {
|
||
let s = M1FieldSource::open(&server, SYMBOL, Some(from_ms), Some(to_ms), f)
|
||
.expect("window overlaps GER40 data");
|
||
sources_b.push(Box::new(s));
|
||
}
|
||
let (h_b, taps_b) = build_harness();
|
||
let series_b = series_from(h_b, &taps_b, sources_b);
|
||
|
||
assert_eq!(
|
||
series_a, series_b,
|
||
"open_ohlc (Timestamp) must reproduce the ms-path open bit-identically (behaviour-preserving, #80/#92)",
|
||
);
|
||
}
|