//! #290 / C28: `aura measure ic --signal --price ` reduces a //! measurement run's two recorded taps to an Information Coefficient + its //! permutation-null significance. Exercises the WIRING and well-formedness over a //! real causal run; the signal-vs-noise math is unit-tested (a look-ahead-engineered //! signal is impossible in a causal run, C2). use std::path::Path; use std::process::Command; const BIN: &str = env!("CARGO_BIN_EXE_aura"); fn temp_cwd(name: &str) -> std::path::PathBuf { let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("aura-cli-measic-{name}")); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).expect("create temp cwd"); dir } /// `examples/r_sma.json` turned MEASUREMENT-shaped with TWO taps (node 0 "signal", /// node 1 "price") and no `bias` output — same closed topology, runs on the built-in /// synthetic stream. (Two-tap authoring mirrors tests/tap_recording.rs.) fn two_tap_blueprint_json() -> String { let path = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let doc = std::fs::read_to_string(path).expect("read examples/r_sma.json"); let mut v: serde_json::Value = serde_json::from_str(&doc).expect("parse r_sma.json"); v["blueprint"]["taps"] = serde_json::json!([ {"name": "signal", "from": {"node": 0, "field": 0}}, {"name": "price", "from": {"node": 1, "field": 0}}, ]); v["blueprint"]["output"] = serde_json::json!([]); serde_json::to_string(&v).expect("re-serialize measurement blueprint") } fn run_measurement(cwd: &Path) { let bp = cwd.join("measurement.json"); std::fs::write(&bp, two_tap_blueprint_json()).expect("write blueprint"); let out = Command::new(BIN) .args(["run", bp.to_str().unwrap()]) .current_dir(cwd) .output() .expect("spawn aura run"); assert!(out.status.success(), "aura run stderr: {}", String::from_utf8_lossy(&out.stderr)); } fn measure_ic(cwd: &Path, run: &str, extra: &[&str]) -> std::process::Output { let mut args = vec!["measure", "ic", run, "--signal", "signal", "--price", "price"]; args.extend_from_slice(extra); Command::new(BIN).args(&args).current_dir(cwd).output().expect("spawn aura measure ic") } #[test] fn measure_ic_emits_a_well_formed_report() { let cwd = temp_cwd("wellformed"); run_measurement(&cwd); let out = measure_ic(&cwd, "sma_signal", &[]); assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr)); let r: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout is IcReport JSON"); assert_eq!(r["run"], "sma_signal"); assert_eq!(r["signal_tap"], "signal"); assert_eq!(r["price_tap"], "price"); assert_eq!(r["horizon"], 1); assert_eq!(r["permutations"], 1000); assert_eq!(r["seed"], 0); assert!(r["n_pairs"].as_u64().unwrap() >= 2, "expected aligned pairs, got {}", r["n_pairs"]); let ic = r["information_coefficient"].as_f64().unwrap(); assert!(ic.is_finite() && (-1.0..=1.0).contains(&ic), "ic = {ic}"); let p = r["overfit_probability"].as_f64().unwrap(); assert!(p > 0.0 && p <= 1.0, "overfit_probability = {p}"); } #[test] fn measure_ic_is_deterministic() { let cwd = temp_cwd("determinism"); run_measurement(&cwd); let a = measure_ic(&cwd, "sma_signal", &["--seed", "9"]); let b = measure_ic(&cwd, "sma_signal", &["--seed", "9"]); assert!(a.status.success() && b.status.success()); assert_eq!(a.stdout, b.stdout, "same seed → byte-identical report"); } #[test] fn measure_ic_unknown_run_errors() { let cwd = temp_cwd("unknownrun"); run_measurement(&cwd); let out = measure_ic(&cwd, "no_such_run", &[]); assert!(!out.status.success(), "an unknown run must exit non-zero"); } /// `--horizon` is real plumbing from the CLI arg through /// `information_coefficient`'s alignment window, not merely echoed into the /// report: a horizon that exceeds the recorded price series collapses every /// signal/forward-return pair, hitting the reduction's documented degenerate /// floor (`n_pairs=0`, `ic=0.0`, `overfit_probability=1.0`, per main.rs's /// `information_coefficient` doc comment) — a floor the unit tests exercise /// only by calling the reduction in-memory, never through CLI parsing + a /// persisted-trace round trip. #[test] fn measure_ic_oversized_horizon_degenerates_through_the_cli() { let cwd = temp_cwd("oversizedhorizon"); run_measurement(&cwd); let out = measure_ic(&cwd, "sma_signal", &["--horizon", "100000000"]); assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr)); let r: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout is IcReport JSON"); assert_eq!(r["horizon"], 100_000_000); assert_eq!(r["n_pairs"], 0, "an oversized horizon aligns no pairs"); assert_eq!(r["information_coefficient"], 0.0, "degenerate floor: ic = 0.0"); assert_eq!(r["overfit_probability"], 1.0, "degenerate floor: overfit_probability = 1.0"); } #[test] fn measure_ic_missing_tap_errors() { let cwd = temp_cwd("missingtap"); run_measurement(&cwd); let out = Command::new(BIN) .args(["measure", "ic", "sma_signal", "--signal", "nope", "--price", "price"]) .current_dir(&cwd) .output() .expect("spawn"); assert!(!out.status.success(), "a missing tap must exit non-zero"); assert!( String::from_utf8_lossy(&out.stderr).contains("nope"), "the error names the missing tap: {}", String::from_utf8_lossy(&out.stderr) ); }