170c6c82dc
Completes the shell-boundary cycle (tasks 10-13): - c28_layering now enumerates the FULL workspace: every crate row is reconciled against its real production [dependencies], a completeness assertion pins the table to the on-disk crates/ set (a new crate can no longer escape the guard silently), the shell/assembly imported-by- nothing rule is asserted, and a shell-content check pins aura-cli to no-[lib] plus a closed allow-list of argv/translation/presentation modules. A new domain module in the shell now fails the suite. - Ledger amendments: C28 gains the assembly position (aura-runner) and the corrected import-rule prose (the ratified aura-campaign -> aura-backtest production edge, #291/#292); phase 3 is marked done with the #297 process::exit residual named; a provenance note records this as structural-debt closure, not a demonstrated downstream blocker. C25 records the control-surface decision: the text artifact vocabulary is canonical, every control surface (CLI executor verbs, a future host, an MCP face, a World program) is a projection/executor over it — the which-projection-next ranking deliberately open on #295. C14 gets the executor-face amendment, C26 the binding-module relocation. - aura_campaign::MemberRunner's doc names the shipped default implementor (aura-runner::DefaultMemberRunner) while the column keeps zero dependency on it. - New library-only E2E fixture (aura-runner/tests/world_member_run_e2e): runs the canonical member recipe over a tiny synthetic archive with no aura-cli in the link graph, and pins C1 determinism (two independently constructed runners produce a byte-identical RunReport). - campaign_run.rs module doc: intra-doc MemberRunner link re-anchored to aura_campaign::MemberRunner (the impl moved out with part 1). Verification: cargo test --workspace green (1473 passed, 0 failed); clippy --workspace --all-targets -D warnings clean; cargo doc clean of NEW warnings (the five unresolved-link warnings in aura-std/aura-backtest predate this cycle byte-identically at the anchor — #288-era doc drift, left for the cycle audit). refs #295
153 lines
6.4 KiB
Rust
153 lines
6.4 KiB
Rust
//! 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::<String, ()>(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<aura_backtest::RunReport, aura_campaign::MemberFault> {
|
|
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
|
|
);
|
|
}
|