//! Property (#295): the canonical member-run recipe — [`DefaultMemberRunner`] //! driven through [`aura_campaign::CellSpec`] — is reachable as a **pure //! library call**: no `aura` binary, no `aura-cli` crate in the dependency //! graph (this test binary links only `aura-runner` + its own deps). This is //! the acceptance evidence `examples/world_member_run.rs` demonstrates by //! printing; here it is pinned by assertion instead of eyeballed output, and //! runs over a tiny synthetic archive rather than skipping when no local host //! archive is mounted. //! //! A second property rides along for free at no extra fixture cost (C1, //! determinism): two independently constructed runners (fresh `DataServer`, //! fresh `DefaultMemberRunner`) driven over byte-identical input produce a //! byte-identical `RunReport` — same input, same run, reproducibly. use std::collections::BTreeMap; use std::io::Write as _; use std::path::{Path, PathBuf}; use aura_campaign::{CellSpec, MemberRunner}; use aura_ingest::DataServer; use aura_runner::{DefaultMemberRunner, Env}; const SYMBOL: &str = "TESTSYM"; // 2024-06-03T08:00:00Z (a real Monday) — arbitrary, fixed, deterministic. const BASE_MS: i64 = 1_717_401_600_000; const N_BARS: i64 = 60; /// A unique scratch directory under the OS temp root, removed on drop — /// mirrors `aura-ingest`'s own `tests::ScratchDir` (pid + atomic counter, no /// `tempfile` dev-dependency needed for one throwaway fixture dir). struct ScratchDir(PathBuf); impl ScratchDir { fn new() -> Self { static COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let dir = std::env::temp_dir() .join(format!("aura-runner-e2e-{}-{n}", std::process::id())); std::fs::create_dir_all(&dir).expect("create scratch dir"); Self(dir) } fn path(&self) -> &Path { &self.0 } } impl Drop for ScratchDir { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } } /// Packs one `RawM1Record`-shaped 48-byte little-endian record — the exact /// on-disk format `data-server` reads (mirrors `aura-ingest`'s and /// `aura-cli`'s own synthetic-archive test helpers). fn pack_record(unix_ms_time: i64, open: f64, high: f64, low: f64, close: f64) -> [u8; 48] { const DELPHI_EPOCH_OFFSET_DAYS: f64 = 25569.0; const MS_PER_DAY: f64 = 86_400_000.0; let dt = unix_ms_time as f64 / MS_PER_DAY + DELPHI_EPOCH_OFFSET_DAYS; let mut rec = [0u8; 48]; rec[0..8].copy_from_slice(&dt.to_le_bytes()); rec[8..16].copy_from_slice(&open.to_le_bytes()); rec[16..24].copy_from_slice(&high.to_le_bytes()); rec[24..32].copy_from_slice(&low.to_le_bytes()); rec[32..40].copy_from_slice(&close.to_le_bytes()); rec[40..44].copy_from_slice(&0.5_f32.to_le_bytes()); rec[44..48].copy_from_slice(&100_i32.to_le_bytes()); rec } /// Writes a tiny, deterministic one-month M1 archive (`N_BARS` consecutive /// 1-minute bars, strictly ascending close so the shipped Donchian breakout /// blueprint actually latches a non-flat bias) plus its geometry sidecar, /// under `dir`. Returns the inclusive `(first_ms, last_ms)` bar span. fn write_tiny_archive(dir: &Path) -> (i64, i64) { let path = dir.join(format!("{SYMBOL}_2024_06.m1")); let file = std::fs::File::create(&path).expect("create synthetic m1 zip"); let mut zip = zip::ZipWriter::new(file); zip.start_file::(format!("{SYMBOL}.bin"), Default::default()) .expect("start synthetic m1 zip entry"); for i in 0..N_BARS { let ms = BASE_MS + i * 60_000; let close = 100.0 + i as f64 * 2.0; // strictly ascending -> a real breakout let open = close - 1.0; let high = close + 0.5; let low = open - 0.5; zip.write_all(&pack_record(ms, open, high, low, close)).expect("write record"); } zip.finish().expect("finish synthetic m1 zip"); std::fs::write( dir.join(format!("{SYMBOL}.meta.json")), "{\"schemaVersion\":2,\"symbol\":\"TESTSYM\",\"digits\":1,\"pipSize\":1,\ \"tickSize\":0.1,\"lotSize\":1,\"baseAsset\":\"TESTSYM\",\"quoteAsset\":\"EUR\"}", ) .expect("write synthetic geometry sidecar"); (BASE_MS, BASE_MS + (N_BARS - 1) * 60_000) } /// One fresh `DefaultMemberRunner` over its own `DataServer` instance pointed /// at `data_dir`, running `cell` end to end. fn run_once( data_dir: &Path, cell: &CellSpec, window_ms: (i64, i64), ) -> Result { let env = Env::std(); let server = std::sync::Arc::new(DataServer::new(data_dir)); let runner = DefaultMemberRunner::new(&env, server, BTreeMap::new(), Vec::new()); runner.run_member(cell, &[], window_ms) } #[test] /// Property: `DefaultMemberRunner::run_member` — the shipped C28 assembly /// recipe — runs a real member backtest to completion through nothing but /// `aura-runner` + `aura-campaign` library calls, and does so deterministically: /// two independently constructed runners over byte-identical archive input /// produce a byte-identical `RunReport` (C1). fn default_member_runner_is_reachable_as_a_pure_library_call_and_is_deterministic() { let scratch = ScratchDir::new(); let (first_ms, last_ms) = write_tiny_archive(scratch.path()); let blueprint_json = include_str!("../../aura-cli/examples/r_breakout.json").to_string(); let cell = CellSpec { strategy_ordinal: 0, strategy_id: aura_research::content_id_of(&blueprint_json), blueprint_json, axes: BTreeMap::new(), instrument: SYMBOL.to_string(), window_ms: (first_ms, last_ms), regime: None, regime_ordinal: 0, }; let report_a = run_once(scratch.path(), &cell, (first_ms, last_ms)) .expect("the member run must succeed over its own tiny archive"); let report_b = run_once(scratch.path(), &cell, (first_ms, last_ms)) .expect("a second, independently constructed runner must succeed identically"); assert_eq!( report_a, report_b, "same cell, same archive, two independent DefaultMemberRunner instances: \ the RunReport must be byte-identical (C1 determinism)" ); assert!( report_a.metrics.total_pips.abs() > 0.0, "the fixture's strictly-ascending closes must drive a non-flat run, else \ the determinism check above is vacuous (both sides equally empty): {:?}", report_a.metrics ); }