77ad977623
Per-cycle fieldtest of the aura-ingest ingestion boundary (issue #7), from the public interface only. 3 examples (transpose/normalize core; close_stream -> deterministic harness run with epoch-ns end-to-end; load_m1_window over real AAPL.US 2006-08 bars + None paths), all green. 0 bugs, 0 friction. Two spec_gaps named for follow-up (neither a code defect; both filed to the tracker): - load_m1_window returns Some(empty M1Columns) — not None — for an in-coverage but bar-empty window. The rustdoc's "None if no data in [from_ms,to_ms]" reads bar-level but the realized boundary is file-level (it inherits data-server's file-skip None). A consumer matching None == "no bars" is wrong for the in-coverage-empty case. -> tighten the load_m1_window rustdoc (one line). - AAPL.US (the carrier/integration-test symbol) has only ~21 M1 bars/month, so the SMA-cross never warms and the end-to-end run yields all-zero (degenerate but valid) metrics. The boundary is correct (C2 warm-up); the milestone's shipped demo data is too thin to show a non-degenerate trace. -> weigh against C22's "newcomer sees a populated trace" before the Walking-skeleton close (document expected density or name a denser demo symbol/window).
182 lines
8.6 KiB
Rust
182 lines
8.6 KiB
Rust
//! Fieldtest c0011 #3 — load_m1_window against real data (axis c).
|
|
//!
|
|
//! Question under test: a downstream researcher opens the local data-server
|
|
//! archive, asks `load_m1_window` for a real symbol/window, gets back SoA
|
|
//! `M1Columns`, takes `close_stream()`, and runs an end-to-end signal-quality
|
|
//! backtest over REAL recorded AAPL.US 2006 M1 close bars — deterministically.
|
|
//! And: the documented `None` path (symbol/window miss) behaves as the rustdoc
|
|
//! says.
|
|
//!
|
|
//! Public-surface facts used (rustdoc + data-server README):
|
|
//! - aura_ingest::load_m1_window(&Arc<DataServer>, symbol, from_ms, to_ms)
|
|
//! -> Option<M1Columns>; "None if the symbol has no data in [from_ms,
|
|
//! to_ms]"; inclusive Unix-ms bounds (data-server's contract).
|
|
//! - data_server::{DataServer, DEFAULT_DATA_PATH}; DataServer::new(base_path);
|
|
//! has_symbol(symbol) -> bool; stream is over Arc<DataServer>.
|
|
//! - close_stream() ascending in timestamp -> Harness::run source shape.
|
|
//!
|
|
//! Skips cleanly (exit 0, prints SKIP) where the local archive is absent, so it
|
|
//! is hermetic on machines without /mnt/tickdata — mirroring the gated
|
|
//! integration test's documented behaviour.
|
|
|
|
use std::path::Path;
|
|
use std::sync::{Arc, mpsc};
|
|
|
|
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
|
|
use aura_engine::{Edge, Harness, SourceSpec, Target, f64_field, summarize};
|
|
use aura_ingest::load_m1_window;
|
|
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
|
|
use data_server::{DEFAULT_DATA_PATH, DataServer};
|
|
|
|
// AAPL.US 2006-08 is present per the carrier. Window the whole month, inclusive.
|
|
const SYMBOL: &str = "AAPL.US";
|
|
// 2006-08-01T00:00:00Z .. 2006-09-01T00:00:00Z in Unix-ms.
|
|
const FROM_MS: i64 = 1_154_390_400_000;
|
|
const TO_MS: i64 = 1_157_068_800_000;
|
|
|
|
fn main() {
|
|
if !Path::new(DEFAULT_DATA_PATH).exists() {
|
|
println!("c0011_3 SKIP: {DEFAULT_DATA_PATH} absent (no local archive)");
|
|
return;
|
|
}
|
|
|
|
let server = Arc::new(DataServer::new(DEFAULT_DATA_PATH));
|
|
if !server.has_symbol(SYMBOL) {
|
|
println!("c0011_3 SKIP: symbol {SYMBOL} not in local archive");
|
|
return;
|
|
}
|
|
|
|
// ---- The happy path: load a real M1 window. ----------------------------
|
|
let cols = match load_m1_window(&server, SYMBOL, FROM_MS, TO_MS) {
|
|
Some(c) => c,
|
|
None => {
|
|
println!("c0011_3 SKIP: {SYMBOL} present but no bars in the 2006-08 window");
|
|
return;
|
|
}
|
|
};
|
|
let n = cols.close.len();
|
|
println!("loaded {n} real M1 bars for {SYMBOL} in 2006-08");
|
|
assert!(n > 0, "a present symbol+window yields at least one bar");
|
|
|
|
// SoA column lengths are all aligned (C7: one index across columns).
|
|
assert_eq!(cols.ts.len(), n, "ts column length matches close");
|
|
assert_eq!(cols.open.len(), n, "open length");
|
|
assert_eq!(cols.high.len(), n, "high length");
|
|
assert_eq!(cols.low.len(), n, "low length");
|
|
assert_eq!(cols.spread.len(), n, "spread length");
|
|
assert_eq!(cols.volume.len(), n, "volume (i64) length");
|
|
|
|
// Timestamps normalized to epoch-ns and inside the requested window
|
|
// (the boundary normalizes Unix-ms -> ns; the window bounds are Unix-ms).
|
|
let from_ns = Timestamp(FROM_MS * 1_000_000);
|
|
let to_ns = Timestamp(TO_MS * 1_000_000);
|
|
for (i, t) in cols.ts.iter().enumerate() {
|
|
assert!(*t >= from_ns && *t <= to_ns, "bar {i} ts {t:?} inside the normalized window");
|
|
}
|
|
|
|
// ---- close_stream is ascending (the C3 precondition Harness::run needs). -
|
|
let price_stream: Vec<(Timestamp, Scalar)> = cols.close_stream();
|
|
assert_eq!(price_stream.len(), n, "one close-stream entry per bar");
|
|
for w in price_stream.windows(2) {
|
|
assert!(w[0].0 <= w[1].0, "real close_stream non-decreasing in timestamp (C3)");
|
|
}
|
|
println!(
|
|
"first bar = (ts {:?}, close {:?}), last = (ts {:?}, close {:?})",
|
|
price_stream[0].0, price_stream[0].1,
|
|
price_stream[n - 1].0, price_stream[n - 1].1,
|
|
);
|
|
|
|
// ---- End-to-end signal-quality run over REAL close bars. ---------------
|
|
let report1 = run_signal_quality(&price_stream);
|
|
let report2 = run_signal_quality(&price_stream); // disjoint second run
|
|
println!("metrics over real AAPL.US 2006-08 close bars = {report1:?}");
|
|
|
|
assert!(report1.0.is_finite(), "total_pips finite over real bars");
|
|
assert!(report1.1.is_finite() && report1.1 >= 0.0, "max_drawdown finite, >= 0 over real bars");
|
|
// Determinism (C1): same recorded input -> bit-identical metrics.
|
|
assert_eq!(report1, report2, "two runs over the same real window are bit-identical (C1)");
|
|
|
|
// ---- The documented None path: a symbol with no data in the window. ----
|
|
// A far-future window for a present symbol -> data-server's own None.
|
|
let future_from = 30_000_000_000_000_i64; // ~year 2920 in Unix-ms
|
|
let future_to = 30_000_086_400_000_i64;
|
|
let miss = load_m1_window(&server, SYMBOL, future_from, future_to);
|
|
assert!(miss.is_none(), "present symbol, empty window -> None (rustdoc)");
|
|
println!("None-path: {SYMBOL} with a far-future window -> {miss:?}");
|
|
|
|
// An unknown symbol -> None as well.
|
|
let miss_sym = load_m1_window(&server, "NO.SUCH.SYMBOL_XYZ", FROM_MS, TO_MS);
|
|
assert!(miss_sym.is_none(), "unknown symbol -> None (rustdoc / data-server contract)");
|
|
println!("None-path: unknown symbol -> {miss_sym:?}");
|
|
|
|
// ---- The grey zone: a window INSIDE the file's date coverage but with no
|
|
// bars in it. The rustdoc says "None if the symbol has no data in
|
|
// [from_ms, to_ms]". A narrow single-day window inside 2006-08 (whose
|
|
// file IS loaded) but containing zero bars returns Some(EMPTY columns),
|
|
// NOT None — data-server's `None` is FILE-level (no file overlaps the
|
|
// window, skipped without I/O), while a loaded file that yields no bar
|
|
// in the exact sub-window transposes to Some(empty). Recorded as a
|
|
// spec_gap: the Some(empty) vs None boundary is not crisp on the public
|
|
// surface. We assert the OBSERVED reading.
|
|
let one_day_from = FROM_MS; // 2006-08-01T00:00Z
|
|
let one_day_to = FROM_MS + 86_400_000; // +1 day
|
|
let narrow = load_m1_window(&server, SYMBOL, one_day_from, one_day_to);
|
|
match &narrow {
|
|
Some(c) if c.close.is_empty() => {
|
|
println!("grey-zone: in-coverage empty sub-window -> Some(EMPTY), not None");
|
|
}
|
|
Some(c) => println!("grey-zone: in-coverage sub-window -> Some({} bars)", c.close.len()),
|
|
None => println!("grey-zone: in-coverage empty sub-window -> None"),
|
|
}
|
|
// Observed on this archive: Some(empty). Asserting the observed reading so
|
|
// the fixture is a faithful record (a future tightening would flip this).
|
|
assert!(
|
|
matches!(&narrow, Some(c) if c.close.is_empty()),
|
|
"in-coverage empty sub-window returns Some(empty), not None (observed reading)"
|
|
);
|
|
|
|
println!("c0011_3 OK: load_m1_window drove a deterministic end-to-end run over real bars; None paths hold");
|
|
}
|
|
|
|
/// SMA-cross -> Exposure -> SimBroker -> Recorder; returns (total_pips,
|
|
/// max_drawdown, exposure_sign_flips) as the summarized metrics tuple.
|
|
fn run_signal_quality(price_stream: &[(Timestamp, Scalar)]) -> (f64, f64, u64) {
|
|
let (eq_tx, eq_rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
|
|
let (exp_tx, exp_rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
|
|
|
|
let mut h = Harness::bootstrap(
|
|
vec![
|
|
Box::new(Sma::new(10)),
|
|
Box::new(Sma::new(30)),
|
|
Box::new(Sub::new()),
|
|
Box::new(Exposure::new(1.0)),
|
|
Box::new(SimBroker::new(0.0001)), // AAPL.US pip_size; reference metadata
|
|
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, eq_tx)),
|
|
Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, exp_tx)),
|
|
],
|
|
vec![SourceSpec {
|
|
kind: ScalarKind::F64,
|
|
targets: vec![
|
|
Target { node: 0, slot: 0 },
|
|
Target { node: 1, slot: 0 },
|
|
Target { node: 4, slot: 1 },
|
|
],
|
|
}],
|
|
vec![
|
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
|
Edge { from: 2, to: 3, slot: 0, from_field: 0 },
|
|
Edge { from: 3, to: 4, slot: 0, from_field: 0 },
|
|
Edge { from: 3, to: 6, slot: 0, from_field: 0 },
|
|
Edge { from: 4, to: 5, slot: 0, from_field: 0 },
|
|
],
|
|
)
|
|
.expect("valid real-bars signal-quality DAG");
|
|
|
|
h.run(vec![price_stream.to_vec()]);
|
|
let equity = f64_field(&eq_rx.try_iter().collect::<Vec<_>>(), 0);
|
|
let exposure = f64_field(&exp_rx.try_iter().collect::<Vec<_>>(), 0);
|
|
let m = summarize(&equity, &exposure);
|
|
(m.total_pips, m.max_drawdown, m.exposure_sign_flips)
|
|
}
|