Compare commits
11 Commits
cfc2d9f5b1
...
6fb7caf929
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fb7caf929 | |||
| 48979193b9 | |||
| a20b6f6003 | |||
| 59f547e313 | |||
| 51096a3212 | |||
| bd6ddf9efe | |||
| 7e1dd25a76 | |||
| cc683fb065 | |||
| 913d18755e | |||
| 1102d776df | |||
| 34987be389 |
@@ -830,6 +830,19 @@ fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
|
||||
.unwrap_or_else(|| "member panicked (non-string payload)".to_string())
|
||||
}
|
||||
|
||||
/// The `SilencedPanic` + `catch_unwind` + `panic_message` recipe above
|
||||
/// (this module's three call sites), exposed for reuse by a member-run caller
|
||||
/// OUTSIDE this crate that drives a bare member run without going through
|
||||
/// `execute` (#278: the synthetic blueprint sweep/walk-forward family
|
||||
/// builders in `aura-runner`, which share this exact bare-member seam but
|
||||
/// carry no #272 fault boundary of their own). Returns the panic payload's
|
||||
/// best-effort message on containment; `AssertUnwindSafe` is the same
|
||||
/// judgement documented on `panic_message` above.
|
||||
pub fn catch_member_panic<R>(f: impl FnOnce() -> R) -> Result<R, String> {
|
||||
let _silence = SilencedPanic::enter();
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).map_err(panic_message)
|
||||
}
|
||||
|
||||
/// Assemble the (CellOutcome, CellRealization) pair for a cell that failed at a
|
||||
/// stage (#272): the realized stage prefix survives in `stages`, `families`/
|
||||
/// `selections` carry whatever already-persisted stages produced (empty for a
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
|
||||
mod exec;
|
||||
pub use exec::{
|
||||
execute, member_fault_prose, CampaignOutcome, CellOutcome, StageFamily, StageSelectionOut,
|
||||
catch_member_panic, execute, member_fault_prose, CampaignOutcome, CellOutcome, StageFamily,
|
||||
StageSelectionOut,
|
||||
};
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -315,8 +315,8 @@ fn present_campaign(
|
||||
for cell in &outcome.record.cells {
|
||||
for (stage_ix, st) in cell.stages.iter().enumerate() {
|
||||
if matches!(&st.survivor_ordinals, Some(v) if v.is_empty()) {
|
||||
eprintln!(
|
||||
"aura: cell {}/{}/[{}, {}]: gate at stage {stage_ix} left no \
|
||||
crate::diag::note!(
|
||||
"cell {}/{}/[{}, {}]: gate at stage {stage_ix} left no \
|
||||
survivors; cell realization truncated",
|
||||
cell.strategy, cell.instrument, cell.window_ms.0, cell.window_ms.1
|
||||
);
|
||||
@@ -333,8 +333,8 @@ fn present_campaign(
|
||||
if let Some(f) = &cell.fault {
|
||||
failed += 1;
|
||||
fail_labels.push(format!("{}: {}", cell.instrument, cell_fault_kind_label(f.kind)));
|
||||
eprintln!(
|
||||
"aura: cell ({}, {}, [{}, {}]) failed at stage {}: {} — recorded, campaign continues",
|
||||
crate::diag::warning!(
|
||||
"cell ({}, {}, [{}, {}]) failed at stage {}: {} — recorded, campaign continues",
|
||||
cell.strategy, cell.instrument, cell.window_ms.0, cell.window_ms.1, f.stage, f.detail
|
||||
);
|
||||
}
|
||||
@@ -388,7 +388,7 @@ fn present_campaign(
|
||||
.expect("campaign run record serializes")
|
||||
);
|
||||
eprintln!(
|
||||
"campaign run {} recorded: {} cells{}",
|
||||
"aura: campaign run {} recorded: {} cells{}",
|
||||
outcome.record.run,
|
||||
outcome.record.cells.len(),
|
||||
if failed == 0 {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//! Stable stderr class markers (C14): `aura: note: <text>` for benign
|
||||
//! diagnostics on a continuing run (exit code unaffected),
|
||||
//! `aura: warning: <text>` for recorded faults the run survives.
|
||||
//! Error lines that accompany a non-zero exit — and plain info lines
|
||||
//! such as the run-record summary — keep the bare `aura:` prefix; for
|
||||
//! errors, the exit-code partition is the machine contract.
|
||||
|
||||
macro_rules! note {
|
||||
($($arg:tt)*) => {
|
||||
eprintln!("aura: note: {}", format_args!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! warning {
|
||||
($($arg:tt)*) => {
|
||||
eprintln!("aura: warning: {}", format_args!($($arg)*))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use {note, warning};
|
||||
|
||||
/// The zero-trade note's message text (#313), pluralization-aware — the
|
||||
/// milestone fieldtest tripped over "all 1 walk-forward windows"
|
||||
/// (captured: `fieldtests/milestone-stderr-honesty/captured/
|
||||
/// example2_singular.err`). Pure over the count so the wording is
|
||||
/// unit-pinned.
|
||||
fn zero_trade_note_text(n_windows: usize) -> String {
|
||||
if n_windows == 1 {
|
||||
"the single walk-forward window recorded zero trades".to_string()
|
||||
} else {
|
||||
format!("all {n_windows} walk-forward windows recorded zero trades")
|
||||
}
|
||||
}
|
||||
|
||||
/// The #313 zero-trade note, shared by both walk-forward paths (the
|
||||
/// synthetic-blueprint path in `main.rs` and the `--real` sugar path in
|
||||
/// `verb_sugar.rs`) so the wording AND the condition live in exactly one
|
||||
/// place. Takes the per-window trade counts; a no-op unless there is at
|
||||
/// least one window and every one of them traded zero times.
|
||||
pub(crate) fn note_zero_trade_windows(mut window_trades: impl ExactSizeIterator<Item = u64>) {
|
||||
let n_windows = window_trades.len();
|
||||
if n_windows > 0 && window_trades.all(|n| n == 0) {
|
||||
note!("{}", zero_trade_note_text(n_windows));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::zero_trade_note_text;
|
||||
|
||||
#[test]
|
||||
/// #278 fieldtest friction: the single-window form must read as
|
||||
/// grammatical English while the plural form keeps the exact phrase
|
||||
/// the e2e pins grep for.
|
||||
fn zero_trade_note_pluralizes() {
|
||||
assert_eq!(
|
||||
zero_trade_note_text(1),
|
||||
"the single walk-forward window recorded zero trades"
|
||||
);
|
||||
assert_eq!(
|
||||
zero_trade_note_text(3),
|
||||
"all 3 walk-forward windows recorded zero trades"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
//! (or the `--strategy r-sma` sugar) and print canonical JSON metrics/manifests;
|
||||
//! this binary authors no built-in harness.
|
||||
|
||||
mod diag;
|
||||
mod render;
|
||||
mod graph_construct;
|
||||
mod campaign_run;
|
||||
@@ -916,6 +917,12 @@ fn run_blueprint_walkforward(
|
||||
for w in &result.windows {
|
||||
println!("{}", family_member_line(&id, &w.run.oos_report));
|
||||
}
|
||||
crate::diag::note_zero_trade_windows(
|
||||
result
|
||||
.windows
|
||||
.iter()
|
||||
.map(|w| w.run.oos_report.metrics.r.as_ref().map_or(0, |m| m.n_trades)),
|
||||
);
|
||||
println!("{}", walkforward_summary_json(&result));
|
||||
}
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ pub fn scaffold_project(spec: &ProjectScaffoldSpec) -> Result<(), String> {
|
||||
Ok(o) if o.status.success() => {
|
||||
commit_all(&spec.target_dir, "aura new scaffold");
|
||||
}
|
||||
_ => eprintln!("aura: warning: git init failed (project created without a repo)"),
|
||||
_ => crate::diag::warning!("git init failed (project created without a repo)"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -324,7 +324,7 @@ pub fn scaffold_node_crate(spec: &ScaffoldSpec) -> Result<(), String> {
|
||||
Ok(o) if o.status.success() => {
|
||||
commit_all(&spec.target_dir, "aura nodes new scaffold");
|
||||
}
|
||||
_ => eprintln!("aura: warning: git init failed (node crate created without a repo)"),
|
||||
_ => crate::diag::warning!("git init failed (node crate created without a repo)"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -357,7 +357,7 @@ fn commit_all(dir: &std::path::Path, message: &str) {
|
||||
.current_dir(dir)
|
||||
.output();
|
||||
if !matches!(add, Ok(o) if o.status.success()) {
|
||||
eprintln!("aura: warning: git add failed (scaffold created without an initial commit)");
|
||||
crate::diag::warning!("git add failed (scaffold created without an initial commit)");
|
||||
return;
|
||||
}
|
||||
let commit = std::process::Command::new("git")
|
||||
@@ -374,8 +374,8 @@ fn commit_all(dir: &std::path::Path, message: &str) {
|
||||
.current_dir(dir)
|
||||
.output();
|
||||
if !matches!(commit, Ok(o) if o.status.success()) {
|
||||
eprintln!(
|
||||
"aura: warning: git commit failed (scaffold created without an initial commit)"
|
||||
crate::diag::warning!(
|
||||
"git commit failed (scaffold created without an initial commit)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,7 +526,7 @@ pub(crate) fn run_walkforward_sugar(
|
||||
)?;
|
||||
|
||||
if let Some(f) = run.outcome.record.cells.iter().find_map(|c| c.fault.as_ref()) {
|
||||
eprintln!("aura: walkforward cell failed at stage {}: {}", f.stage, f.detail);
|
||||
crate::diag::warning!("walkforward cell failed at stage {}: {}", f.stage, f.detail);
|
||||
return Ok(1);
|
||||
}
|
||||
|
||||
@@ -549,6 +549,9 @@ pub(crate) fn run_walkforward_sugar(
|
||||
summary_axes.push("stop_length".to_string());
|
||||
summary_axes.push("stop_k".to_string());
|
||||
}
|
||||
crate::diag::note_zero_trade_windows(
|
||||
wf.reports.iter().map(|r| r.metrics.r.as_ref().map_or(0, |m| m.n_trades)),
|
||||
);
|
||||
println!("{}", crate::walkforward_summary_json_from_reports(&wf.reports, &summary_axes));
|
||||
|
||||
// Trace persistence runs LAST (mirroring `present_campaign`'s ordering,
|
||||
@@ -690,7 +693,7 @@ pub(crate) fn run_mc_sugar(
|
||||
)?;
|
||||
|
||||
if let Some(f) = run.outcome.record.cells.iter().find_map(|c| c.fault.as_ref()) {
|
||||
eprintln!("aura: mc cell failed at stage {}: {}", f.stage, f.detail);
|
||||
crate::diag::warning!("mc cell failed at stage {}: {}", f.stage, f.detail);
|
||||
return Ok(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -4782,6 +4782,234 @@ fn aura_walkforward_over_a_blueprint_persists_a_family() {
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// E2E (#313, Path 2 — synthetic): a walk-forward whose every window records
|
||||
/// zero trades emits exactly one `aura: note: all N walk-forward windows
|
||||
/// recorded zero trades` on stderr; exit stays 0 and the summary JSON is
|
||||
/// unchanged (a null result is a valid research result, #198). Equal
|
||||
/// fast/slow lengths make the SMA difference constantly zero, so the bias
|
||||
/// never leaves 0 and no window trades.
|
||||
#[test]
|
||||
fn aura_walkforward_all_zero_trade_windows_emit_one_note() {
|
||||
let cwd = temp_cwd("blueprint-walkforward-zero-trade-note");
|
||||
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["walkforward", &bp, "--axis", "sma_signal.fast.length=8",
|
||||
"--axis", "sma_signal.slow.length=8", "--name", "wfzero"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura walkforward (degenerate)");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
assert_eq!(out.status.code(), Some(0), "exit stays 0: {stderr}");
|
||||
assert!(stdout.contains("\"walkforward\""), "summary line unchanged: {stdout}");
|
||||
assert_eq!(
|
||||
stderr.matches("walk-forward windows recorded zero trades").count(),
|
||||
1,
|
||||
"exactly one zero-trade note: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
stderr.contains("aura: note: all "),
|
||||
"the note carries the class marker: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("aura: warning: "),
|
||||
"a benign degenerate run emits no warning lines: {stderr}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// E2E (#313 negative twin): a walk-forward with at least one traded window
|
||||
/// emits no zero-trade note.
|
||||
#[test]
|
||||
fn aura_walkforward_with_trades_emits_no_zero_trade_note() {
|
||||
let cwd = temp_cwd("blueprint-walkforward-traded-no-note");
|
||||
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["walkforward", &bp, "--axis", "sma_signal.fast.length=2,3",
|
||||
"--axis", "sma_signal.slow.length=4,6", "--name", "wftrades"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura walkforward (trading)");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
assert_eq!(out.status.code(), Some(0), "exit 0: {stderr}");
|
||||
assert!(
|
||||
!stderr.contains("recorded zero trades"),
|
||||
"a traded run emits no zero-trade note: {stderr}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// E2E (#278 decision: same fault class, same verb, same surface => same
|
||||
/// treatment): a member fault on the SYNTHETIC walk-forward path is CONTAINED
|
||||
/// exactly like the real/campaign path contains it (#272) — it surfaces as an
|
||||
/// `aura: warning: ` class-marked stderr line naming the member fault and the
|
||||
/// process takes the deliberate failed-cells exit 3 (C14 partition,
|
||||
/// `exit_on_campaign_result` precedent). It never escapes as an uncaught Rust
|
||||
/// panic: no exit 101, no `panicked at`, no internal `crates/` source path on
|
||||
/// the consumer's stderr.
|
||||
#[test]
|
||||
fn aura_walkforward_synthetic_member_panic_is_contained() {
|
||||
let cwd = temp_cwd("blueprint-walkforward-member-panic-contained");
|
||||
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
// length=0 poisons the member: `Sma::new` asserts "SMA length must be >= 1"
|
||||
// during member compile — the same fault class the real path records as
|
||||
// "a member panicked: …" and survives.
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["walkforward", &bp, "--axis", "sma_signal.fast.length=0", "--name", "wfpanic"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura walkforward (poison member)");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
assert!(
|
||||
stderr.lines().any(|l| l.contains("aura: warning: ") && l.contains("SMA length must be >= 1")),
|
||||
"the member fault surfaces as one class-marked warning naming the fault: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("panicked at"),
|
||||
"no raw panic report reaches the consumer: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("crates/"),
|
||||
"no internal source path leaks to the consumer: {stderr}"
|
||||
);
|
||||
assert_eq!(
|
||||
out.status.code(),
|
||||
Some(3),
|
||||
"a contained member fault is the deliberate failed-cells exit 3, never 101: {stderr}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// E2E (#278 decision, sweep twin): the synthetic `aura sweep` shares the
|
||||
/// same bare-member seam (`run_blueprint_member` inside the family builders'
|
||||
/// sweep closures, no #272 fault boundary), so the same poison must be
|
||||
/// contained the same way — warning + exit 3, no panic escape.
|
||||
#[test]
|
||||
fn aura_sweep_synthetic_member_panic_is_contained() {
|
||||
let cwd = temp_cwd("blueprint-sweep-member-panic-contained");
|
||||
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["sweep", &bp, "--axis", "sma_signal.fast.length=0", "--name", "swpanic"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura sweep (poison member)");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
assert!(
|
||||
stderr.lines().any(|l| l.contains("aura: warning: ") && l.contains("SMA length must be >= 1")),
|
||||
"the member fault surfaces as one class-marked warning naming the fault: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("panicked at") && !stderr.contains("crates/"),
|
||||
"no raw panic report or internal source path leaks: {stderr}"
|
||||
);
|
||||
assert_eq!(
|
||||
out.status.code(),
|
||||
Some(3),
|
||||
"a contained member fault is the deliberate failed-cells exit 3, never 101: {stderr}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// E2E (#278 decision, mc twin): the synthetic `aura mc` shares the same
|
||||
/// bare-member seam as its sweep/walkforward siblings (`run_blueprint_member`
|
||||
/// inside `blueprint_mc_family`'s per-seed draw closure, no #272 fault
|
||||
/// boundary), so the same member-compile fault class must be contained the
|
||||
/// same way — one `aura: warning: ` line naming the fault, no raw panic text
|
||||
/// or internal `crates/` path on stderr, deliberate exit 3 (C14), never 101.
|
||||
/// mc takes only CLOSED blueprints, so the poison is BOUND (`length=0` on the
|
||||
/// SMA add-op) rather than a poison axis: the test builds its minimal 4-op
|
||||
/// poison blueprint inline via `aura graph build`. Two seeds so the parallel
|
||||
/// draws join before the lowest-seed fault resolves (the shipped sweep
|
||||
/// contract's thread-order-independent shape, C1).
|
||||
#[test]
|
||||
fn aura_mc_synthetic_member_panic_is_contained() {
|
||||
let cwd = temp_cwd("blueprint-mc-member-panic-contained");
|
||||
// Minimal poison: each op is load-bearing (source: the close role; add+bind:
|
||||
// the length=0 that trips `Sma::new`'s "SMA length must be >= 1" assert at
|
||||
// member compile; feed: wiring so build validation passes; expose: the bias
|
||||
// contract). Bound length keeps the blueprint CLOSED, as `aura mc` requires.
|
||||
let ops = r#"[
|
||||
{"op":"source","role":"close","kind":"F64"},
|
||||
{"op":"add","type":"SMA","name":"sma","bind":{"length":{"I64":0}}},
|
||||
{"op":"feed","role":"close","into":["sma.series"]},
|
||||
{"op":"expose","from":"sma.value","as":"bias"}
|
||||
]"#;
|
||||
let ops_path = cwd.join("poison.ops.json");
|
||||
std::fs::write(&ops_path, ops).expect("write poison op-list");
|
||||
let bp_path = cwd.join("poison.bp.json");
|
||||
let build = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["graph", "build"])
|
||||
.stdin(std::fs::File::open(&ops_path).expect("open poison op-list"))
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura graph build (poison blueprint)");
|
||||
assert_eq!(
|
||||
build.status.code(),
|
||||
Some(0),
|
||||
"poison blueprint builds (the fault is a member-COMPILE fault, not a build refusal): {}",
|
||||
String::from_utf8_lossy(&build.stderr)
|
||||
);
|
||||
std::fs::write(&bp_path, &build.stdout).expect("write poison blueprint");
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["mc", bp_path.to_str().expect("utf-8 path"), "--seeds", "2"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura mc (poison member)");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
assert!(
|
||||
stderr.lines().any(|l| l.contains("aura: warning: ") && l.contains("SMA length must be >= 1")),
|
||||
"the member fault surfaces as one class-marked warning naming the fault: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("panicked at") && !stderr.contains("crates/"),
|
||||
"no raw panic report or internal source path leaks: {stderr}"
|
||||
);
|
||||
assert_eq!(
|
||||
out.status.code(),
|
||||
Some(3),
|
||||
"a contained member fault is the deliberate failed-cells exit 3, never 101: {stderr}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// E2E (#313, Path 1 — the real/sugar walk-forward): the degenerate
|
||||
/// equal-length config over the shared GER40 archive emits the same
|
||||
/// zero-trade note. Gated on the archive; skips cleanly on a data refusal.
|
||||
#[test]
|
||||
fn walkforward_real_all_zero_trade_windows_emit_the_note() {
|
||||
const FROM_MS: &str = "1735689600000";
|
||||
const TO_MS: &str = "1767225599000";
|
||||
let (cwd, _g) = fresh_project();
|
||||
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"walkforward", &fixture, "--real", "GER40",
|
||||
"--axis", "sma_signal.fast.length=5", "--axis", "sma_signal.slow.length=5",
|
||||
"--stop-length", "14", "--stop-k", "2.0",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura walkforward (real degenerate)");
|
||||
if out.status.code() == Some(1) {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
|
||||
"exit 1 must be a data refusal, got: {stderr}"
|
||||
);
|
||||
eprintln!("skip: no local GER40 data");
|
||||
return;
|
||||
}
|
||||
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
assert_eq!(
|
||||
stderr.matches("walk-forward windows recorded zero trades").count(),
|
||||
1,
|
||||
"exactly one zero-trade note on the real path: {stderr}"
|
||||
);
|
||||
assert!(stderr.contains("aura: note: all "), "class marker: {stderr}");
|
||||
}
|
||||
|
||||
/// E2E (#173): a CLOSED blueprint with no --axis (nothing to re-fit) is a usage
|
||||
/// error (exit 2), naming that >= 1 --axis is required.
|
||||
#[test]
|
||||
|
||||
@@ -212,6 +212,58 @@ fn attached_crate_namespace_resolves_after_build() {
|
||||
);
|
||||
}
|
||||
|
||||
/// E2E (#278): loading an attached node crate whose `src/` is newer than its
|
||||
/// built dylib emits exactly one `aura: warning: ` class-marked line naming
|
||||
/// the stale dylib, and still succeeds (the run proceeds with the existing
|
||||
/// dylib rather than refusing) — staleness is a continuing-run warning, never
|
||||
/// a fault. A freshly built dylib (no touched source since) emits none. Real
|
||||
/// `cargo build` + a real `libloading::Library::new` load, exercising
|
||||
/// `aura_runner::project::load_crate`'s actual call site rather than the pure
|
||||
/// `stale_warning` helper alone.
|
||||
#[test]
|
||||
fn attached_crate_load_warns_when_source_outruns_the_built_dylib() {
|
||||
let cwd = temp_cwd("stale-warn");
|
||||
assert!(aura(&["new", "lab"], &cwd).status.success());
|
||||
let proj = cwd.join("lab");
|
||||
let engine = env!("CARGO_MANIFEST_DIR").to_string() + "/../..";
|
||||
assert!(aura(&["nodes", "new", "lab-nodes", "--engine-path", &engine], &proj).status.success());
|
||||
let crate_root = cwd.join("lab-nodes");
|
||||
let build = Command::new("cargo").arg("build").current_dir(&crate_root).output().expect("cargo build");
|
||||
assert!(build.status.success(), "{}", String::from_utf8_lossy(&build.stderr));
|
||||
|
||||
// Fresh build, untouched source: no staleness warning.
|
||||
let fresh = aura(&["graph", "introspect", "--vocabulary"], &proj);
|
||||
assert!(fresh.status.success(), "stderr: {}", String::from_utf8_lossy(&fresh.stderr));
|
||||
assert!(
|
||||
!String::from_utf8_lossy(&fresh.stderr).contains("may be stale"),
|
||||
"a freshly built dylib is not stale: {}",
|
||||
String::from_utf8_lossy(&fresh.stderr)
|
||||
);
|
||||
|
||||
// Bump `src/lib.rs`'s mtime a full minute past "now" (comfortably past the
|
||||
// dylib's just-completed build time) without rebuilding — the exact
|
||||
// "source outran the dylib, no rebuild yet" shape `stale_warning` guards.
|
||||
let lib_rs = crate_root.join("src/lib.rs");
|
||||
let f = std::fs::File::open(&lib_rs).expect("open lib.rs");
|
||||
f.set_modified(std::time::SystemTime::now() + std::time::Duration::from_secs(60))
|
||||
.expect("bump lib.rs mtime past the dylib's build time");
|
||||
|
||||
let stale = aura(&["graph", "introspect", "--vocabulary"], &proj);
|
||||
assert!(
|
||||
stale.status.success(),
|
||||
"staleness is a warning, not a refusal: {}",
|
||||
String::from_utf8_lossy(&stale.stderr)
|
||||
);
|
||||
let stderr = String::from_utf8_lossy(&stale.stderr);
|
||||
assert_eq!(
|
||||
stderr.matches("may be stale").count(),
|
||||
1,
|
||||
"exactly one staleness warning: {stderr}"
|
||||
);
|
||||
assert!(stderr.contains("aura: warning: "), "carries the warning class marker (#278): {stderr}");
|
||||
assert!(stderr.contains("run `cargo build` to refresh"), "names the remedy: {stderr}");
|
||||
}
|
||||
|
||||
/// Property (#316 self-description): the scaffold's own seed node ships a
|
||||
/// `doc:` line that itself passes the C29 shape gate (`aura_core::doc_gate`)
|
||||
/// — non-empty and not a bare restatement of the type name. This exact text
|
||||
|
||||
@@ -1452,6 +1452,19 @@ const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
|
||||
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
|
||||
}"#;
|
||||
|
||||
/// A gate no realized sweep member can ever satisfy (`n_trades ge 999999`) —
|
||||
/// deterministically empties `survivor_ordinals` regardless of the fixture's
|
||||
/// actual trade counts, driving the #278 zero-survivor NOTE marker.
|
||||
const GATE_EMPTIES_PROCESS_DOC: &str = r#"{
|
||||
"format_version": 1,
|
||||
"kind": "process",
|
||||
"name": "gate-empties",
|
||||
"pipeline": [
|
||||
{ "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" },
|
||||
{ "block": "std::gate", "all": [ { "metric": "n_trades", "cmp": "ge", "value": 999999.0 } ] }
|
||||
]
|
||||
}"#;
|
||||
|
||||
/// The full annotated shape for the gated e2e: sweep -> gate -> walk_forward
|
||||
/// -> monte_carlo. The gate (`n_trades ge 0`) passes every member, so
|
||||
/// walk-forward always has survivors; the mc annotator bootstraps the pooled
|
||||
@@ -1978,6 +1991,57 @@ fn campaign_run_synthetic_e2e_parallel_instruments_contains_a_real_per_cell_faul
|
||||
assert_eq!(cells[1]["instrument"].as_str(), Some("SYMB"), "doc order preserved: {line}");
|
||||
assert!(cells[0]["fault"].is_null(), "SYMA has data in January, its cell succeeds: {line}");
|
||||
assert!(!cells[1]["fault"].is_null(), "SYMB has no January data, its cell is faulted: {line}");
|
||||
assert!(
|
||||
out.contains("aura: warning: cell ("),
|
||||
"the failed-cell line carries the warning class marker (#278): {out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// #278 sibling of the warning-class pin above: a gate that empties a cell's
|
||||
/// survivors (never a fault, #198 decision 8) emits the NOTE class marker,
|
||||
/// not the bare `aura:` prefix nor `warning:`. `GATE_EMPTIES_PROCESS_DOC`'s
|
||||
/// impossible threshold guarantees zero survivors regardless of the
|
||||
/// fixture's actual sweep-member trade counts. Second property pinned on the
|
||||
/// same invocation: the run's terminal "recorded" summary line — previously
|
||||
/// printed with NO `aura:` prefix at all — now carries it too, so every
|
||||
/// stderr line this command emits (per-cell class markers AND the run-level
|
||||
/// summary) shares the one `aura:`-rooted vocabulary; a regression that
|
||||
/// reverted just this one un-tagged `eprintln!` back to a bare line would
|
||||
/// not be caught by the note-marker assertion above.
|
||||
#[test]
|
||||
fn campaign_run_synthetic_e2e_gate_emptied_cell_carries_the_note_marker() {
|
||||
let (dir, _fixture) = fresh_project_with_data();
|
||||
let runs_dir = dir.join("runs");
|
||||
std::fs::remove_dir_all(&runs_dir).ok();
|
||||
let _cleanup = ScratchGuard(vec![
|
||||
ScratchPath::Dir(runs_dir.clone()),
|
||||
ScratchPath::File(dir.join("gateempty.process.json")),
|
||||
ScratchPath::File(dir.join("gateempty.campaign.json")),
|
||||
]);
|
||||
let bp_id = seed_blueprint(&dir, "campaign-run-gateempty-seed");
|
||||
let proc_id =
|
||||
register_process_doc(&dir, "gateempty.process.json", GATE_EMPTIES_PROCESS_DOC);
|
||||
let doc = campaign_doc_json_for(
|
||||
"SYMA",
|
||||
&bp_id,
|
||||
&proc_id,
|
||||
(1704844800000, 1705708800000),
|
||||
"",
|
||||
"",
|
||||
);
|
||||
write_doc(&dir, "gateempty.campaign.json", &doc);
|
||||
|
||||
let (out, code) =
|
||||
run_code_in(&dir, &["campaign", "run", "gateempty.campaign.json"]);
|
||||
assert_eq!(code, Some(0), "a zero-survivor cell is a valid result, not a fault: {out}");
|
||||
assert!(
|
||||
out.contains("aura: note: cell "),
|
||||
"the gate-emptied cell line carries the note class marker (#278): {out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains("aura: campaign run "),
|
||||
"the run-level recorded summary line also carries the aura: prefix (#278): {out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The v2 boundary, campaign side: a generalize-bearing process is an
|
||||
@@ -3683,7 +3747,7 @@ fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() {
|
||||
|
||||
assert!(
|
||||
out.contains(
|
||||
"aura: tap \"net_r_equity\" needs a cost model; add a cost block to the campaign document; skipped"
|
||||
"aura: note: tap \"net_r_equity\" needs a cost model; add a cost block to the campaign document; skipped"
|
||||
),
|
||||
"the unproducible tap gets a named, one-time note with the remedy: {out}"
|
||||
);
|
||||
@@ -3786,7 +3850,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
|
||||
assert_eq!(code_gross, Some(0), "cost-less baseline run: {out_gross}");
|
||||
assert!(
|
||||
out_gross.contains(
|
||||
"aura: tap \"net_r_equity\" needs a cost model; add a cost block to the campaign document; skipped"
|
||||
"aura: note: tap \"net_r_equity\" needs a cost model; add a cost block to the campaign document; skipped"
|
||||
),
|
||||
"the cost-less run skips the net tap with the remedy: {out_gross}"
|
||||
);
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
//! CLI shell still needs its own for call sites outside any family builder).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use aura_campaign::{catch_member_panic, member_fault_prose, MemberFault};
|
||||
use aura_composites::StopRule;
|
||||
use aura_core::{Cell, ParamSpec, Scalar, Timestamp};
|
||||
use aura_engine::{
|
||||
@@ -315,7 +316,10 @@ pub fn select_winner(
|
||||
/// probe. `SweepBinder::sweep_with_lattice`'s own `resolve_axes`/arity/kind checks all
|
||||
/// run BEFORE this closure is invoked per grid point, so its body never influences the
|
||||
/// validation outcome — only its signature (`Fn(&[Cell]) -> RunReport`) needs to
|
||||
/// satisfy the terminal, at O(1) cost per point instead of a full member run.
|
||||
/// satisfy the terminal, at O(1) cost per point instead of a full member run. Reused
|
||||
/// (#278) as the discarded slot value for a member run [`catch_member_panic`]
|
||||
/// contains: the caller always resolves the captured fault and exits before this
|
||||
/// value is ever rendered or persisted, so its content is equally irrelevant there.
|
||||
fn axis_grid_probe_report() -> RunReport {
|
||||
RunReport {
|
||||
manifest: RunManifest {
|
||||
@@ -334,6 +338,48 @@ fn axis_grid_probe_report() -> RunReport {
|
||||
}
|
||||
}
|
||||
|
||||
/// The LOWEST enumeration index's captured member-panic message, found by
|
||||
/// walking `family.points` — guaranteed by `assemble_sweep`'s own
|
||||
/// `collect()` to be in grid-enumeration order regardless of which worker
|
||||
/// finished first (C1) — for the first point whose params match a captured
|
||||
/// fault. Thread-order-independent by construction (mirrors the lowest-
|
||||
/// index-fault convention `aura-campaign::exec::run_members` establishes for
|
||||
/// the identical #272 seam), without needing a separate numeric-index
|
||||
/// side-channel: the returned family already carries the points in order.
|
||||
fn lowest_point_fault(family: &SweepFamily, faults: Mutex<Vec<(Vec<Cell>, String)>>) -> Option<String> {
|
||||
let captured = faults.into_inner().expect("fault capture lock");
|
||||
family
|
||||
.points
|
||||
.iter()
|
||||
.find_map(|pt| captured.iter().find(|(p, _)| *p == pt.params).map(|(_, m)| m.clone()))
|
||||
}
|
||||
|
||||
/// The LOWEST seed's captured member-panic message (#278), found by walking
|
||||
/// `family.draws` — `monte_carlo`'s own contract guarantees seed-**input**
|
||||
/// order regardless of thread completion (C1) — for the first draw whose
|
||||
/// seed matches a captured fault. Thread-order-independent by construction,
|
||||
/// mirroring [`lowest_point_fault`]'s and `lowest_window_fault`'s identical
|
||||
/// convention one level over (per-point sweep / per-window walk-forward).
|
||||
fn lowest_seed_fault(family: &McFamily, faults: Mutex<Vec<(u64, String)>>) -> Option<String> {
|
||||
let captured = faults.into_inner().expect("fault capture lock");
|
||||
family
|
||||
.draws
|
||||
.iter()
|
||||
.find_map(|d| captured.iter().find(|(s, _)| *s == d.seed).map(|(_, m)| m.clone()))
|
||||
}
|
||||
|
||||
/// Render + exit(3) on a contained member panic (#278: the deliberate
|
||||
/// failed-cells exit, C14, never the raw 101 an uncontained panic would
|
||||
/// yield) — reuses `aura_campaign::member_fault_prose`'s established
|
||||
/// "a member panicked: …" wording rather than re-wording it, and the
|
||||
/// `aura: warning: ` class marker (#278 decision) the CLI's own `diag`
|
||||
/// macro would emit, hand-written here since that macro is presentation-
|
||||
/// crate-private (#295 boundary) and this fires from `aura-runner`.
|
||||
fn exit_on_member_panic(msg: &str) -> ! {
|
||||
eprintln!("aura: warning: {}", member_fault_prose(&MemberFault::Panic(msg.to_string())));
|
||||
std::process::exit(3);
|
||||
}
|
||||
|
||||
/// Validate the `--axis` grid against `doc`'s wrapped param space WITHOUT running any
|
||||
/// member (#253). Reuses the SAME strict, erroring axis-name check
|
||||
/// `blueprint_sweep_over` performs (`override_paths` — single-sourced, so the two
|
||||
@@ -426,15 +472,35 @@ pub fn blueprint_sweep_family(
|
||||
for (n, vals) in iter {
|
||||
binder = binder.axis(n, vals.clone());
|
||||
}
|
||||
binder
|
||||
// #278: `run_blueprint_member` runs bare here (no #272 fault boundary of its
|
||||
// own), so a member-compile panic (e.g. an `Sma::new` length assert) would
|
||||
// otherwise unwind straight through this sweep to an uncaught exit 101 —
|
||||
// contained the same way the real/campaign path contains it, via
|
||||
// `catch_member_panic` + a per-point capture, resolved to the lowest
|
||||
// enumeration index's message after the sweep joins (thread-order-
|
||||
// independent, C1).
|
||||
let faults: Mutex<Vec<(Vec<Cell>, String)>> = Mutex::new(Vec::new());
|
||||
let family = binder
|
||||
.sweep(|point| {
|
||||
// fresh per-member graph (Composite is !Clone, reload per member) run through
|
||||
// the shared reduce-mode member path — the same fn reproduction re-runs.
|
||||
run_blueprint_member(reload(doc), point, &space, data.run_sources(env, &binding.columns()), window, 0, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
|
||||
match catch_member_panic(|| {
|
||||
run_blueprint_member(reload(doc), point, &space, data.run_sources(env, &binding.columns()), window, 0, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
|
||||
}) {
|
||||
Ok(report) => report,
|
||||
Err(msg) => {
|
||||
faults.lock().expect("fault capture lock").push((point.to_vec(), msg));
|
||||
axis_grid_probe_report()
|
||||
}
|
||||
}
|
||||
})
|
||||
// render the sweep terminal's BindError to prose (#247), the fn's String error
|
||||
// contract — never the raw Debug struct.
|
||||
.map_err(|e| render_bind_error(&e))
|
||||
.map_err(|e| render_bind_error(&e))?;
|
||||
if let Some(msg) = lowest_point_fault(&family, faults) {
|
||||
exit_on_member_panic(&msg);
|
||||
}
|
||||
Ok(family)
|
||||
}
|
||||
|
||||
/// Sweep the LOADED blueprint over the user `--axis` grid on an in-sample window
|
||||
@@ -447,10 +513,19 @@ pub fn blueprint_sweep_family(
|
||||
/// its walk-forward in-sample twin); an axis matching neither space is refused
|
||||
/// (wrapped as `BindError::UnknownKnob`, the honest replacement for the retired
|
||||
/// "fully bound; nothing to sweep" refusal) before any member runs.
|
||||
///
|
||||
/// The returned `Option<String>` (#278) is this fn's OWN resolved member-panic
|
||||
/// capture: this fn's nested sweep runs inside `blueprint_walkforward_family`'s
|
||||
/// per-window parallel closure, so a contained member panic here must NOT
|
||||
/// `eprintln!`+`exit` on the spot (that would race across windows, the exact
|
||||
/// `#177` duplicated-rejection class the dispatch-boundary axis pre-flight
|
||||
/// above already avoids) — captured internally, resolved to the lowest-index
|
||||
/// message via [`lowest_point_fault`] before returning, for the caller to push
|
||||
/// onto its own per-window fault list only after every window has joined.
|
||||
pub fn blueprint_sweep_over(
|
||||
doc: &str, axes: &[(String, Vec<Scalar>)], from: Timestamp, to: Timestamp, data: &DataSource,
|
||||
env: &Env, binding: &ResolvedBinding,
|
||||
) -> Result<(SweepFamily, Vec<usize>), BindError> {
|
||||
) -> Result<(SweepFamily, Vec<usize>, Option<String>), BindError> {
|
||||
let reload = |d: &str| {
|
||||
blueprint_from_json(d, &|t| env.resolve(t))
|
||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
|
||||
@@ -476,11 +551,22 @@ pub fn blueprint_sweep_over(
|
||||
for (n, vals) in iter {
|
||||
binder = binder.axis(n, vals.clone());
|
||||
}
|
||||
binder.sweep_with_lattice(|point| {
|
||||
let faults: Mutex<Vec<(Vec<Cell>, String)>> = Mutex::new(Vec::new());
|
||||
let (family, lattice) = binder.sweep_with_lattice(|point| {
|
||||
let sources = data.windowed_sources(from, to, env, &binding.columns());
|
||||
let window = window_of(&sources).expect("non-empty in-sample window");
|
||||
run_blueprint_member(reopen_all(reload(doc), &overrides), point, &space, sources, window, 0, pip, &topo, env, DEFAULT_STOP, binding, &[], NO_INSTRUMENT_CONTEXT)
|
||||
})
|
||||
match catch_member_panic(|| {
|
||||
run_blueprint_member(reopen_all(reload(doc), &overrides), point, &space, sources, window, 0, pip, &topo, env, DEFAULT_STOP, binding, &[], NO_INSTRUMENT_CONTEXT)
|
||||
}) {
|
||||
Ok(report) => report,
|
||||
Err(msg) => {
|
||||
faults.lock().expect("fault capture lock").push((point.to_vec(), msg));
|
||||
axis_grid_probe_report()
|
||||
}
|
||||
}
|
||||
})?;
|
||||
let fault = lowest_point_fault(&family, faults);
|
||||
Ok((family, lattice, fault))
|
||||
}
|
||||
|
||||
/// Run the winner params over an out-of-sample window `[from,to]` on the loaded
|
||||
@@ -509,6 +595,37 @@ pub fn run_oos_blueprint(
|
||||
(Vec::new(), report)
|
||||
}
|
||||
|
||||
/// A discarded placeholder [`WindowRun`] for a window whose IS refit or OOS
|
||||
/// run was contained after a member panic (#278): `chosen_params` matches
|
||||
/// `space`'s arity with inert zero cells (the `assemble_walk_forward` arity
|
||||
/// invariant), `oos_equity` is empty (an empty segment leaves the stitched
|
||||
/// curve unbroken, same convention as a genuinely equity-less window), and
|
||||
/// the report reuses [`axis_grid_probe_report`]'s zero-cost placeholder.
|
||||
/// Never rendered: the caller always resolves the captured window fault and
|
||||
/// exits before touching a faulted window's run.
|
||||
fn placeholder_window_run(space: &[ParamSpec]) -> WindowRun {
|
||||
WindowRun {
|
||||
chosen_params: space.iter().map(|_| Cell::from_i64(0)).collect(),
|
||||
oos_equity: Vec::new(),
|
||||
oos_report: axis_grid_probe_report(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The LOWEST roll-order window's captured member-panic message (#278),
|
||||
/// found by walking `result.windows` — guaranteed by `assemble_walk_forward`
|
||||
/// to be in roll order regardless of thread completion (C1) — for the first
|
||||
/// window whose bounds match a captured fault. Thread-order-independent by
|
||||
/// construction, mirroring [`lowest_point_fault`]'s identical convention one
|
||||
/// level down (per-point within one window's IS sweep) and
|
||||
/// `aura-campaign::exec`'s own lowest-index walk-forward fault attribution.
|
||||
fn lowest_window_fault(result: &WalkForwardResult, faults: Mutex<Vec<(WindowBounds, String)>>) -> Option<String> {
|
||||
let captured = faults.into_inner().expect("fault capture lock");
|
||||
result
|
||||
.windows
|
||||
.iter()
|
||||
.find_map(|w| captured.iter().find(|(b, _)| *b == w.bounds).map(|(_, m)| m.clone()))
|
||||
}
|
||||
|
||||
/// The loaded-blueprint IS-refit walk-forward: per IS window, re-optimize the
|
||||
/// blueprint over the user `--axis` grid, select by `sqn_normalized`, run the
|
||||
/// winner OOS, reusing the generic `walk_forward` driver + `select_winner`;
|
||||
@@ -568,18 +685,45 @@ pub fn blueprint_walkforward_family(
|
||||
eprintln!("aura: {}", render_bind_error(&e));
|
||||
std::process::exit(2);
|
||||
}
|
||||
walk_forward(roller, space.clone(), |w: WindowBounds| {
|
||||
let (is_family, lattice) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env, &binding)
|
||||
// #278: `blueprint_sweep_over`'s inner sweep and `run_oos_blueprint`'s single
|
||||
// member run both drive `run_blueprint_member` bare, so either could
|
||||
// otherwise unwind an uncaught member-compile panic straight through this
|
||||
// window closure. Both are contained (`blueprint_sweep_over` resolves its
|
||||
// own capture and returns it; `run_oos_blueprint` via `catch_member_panic`
|
||||
// directly) and captured PER WINDOW here rather than printed on the spot —
|
||||
// this closure runs across windows in parallel (`walk_forward`), so an
|
||||
// inline `eprintln!`+`exit` would race the same way the axis pre-flight
|
||||
// above was written to avoid (#177). The lowest roll-order window's fault
|
||||
// is resolved once, after every window has joined.
|
||||
let window_faults: Mutex<Vec<(WindowBounds, String)>> = Mutex::new(Vec::new());
|
||||
let result = walk_forward(roller, space.clone(), |w: WindowBounds| {
|
||||
let (is_family, lattice, is_fault) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env, &binding)
|
||||
.expect("axes validated in the dispatch-boundary pre-flight");
|
||||
if let Some(msg) = is_fault {
|
||||
window_faults.lock().expect("fault capture lock").push((w, msg));
|
||||
return placeholder_window_run(&space);
|
||||
}
|
||||
let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, Some(&lattice)) {
|
||||
Ok(v) => v,
|
||||
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
|
||||
};
|
||||
let (oos_equity, mut oos_report) =
|
||||
run_oos_blueprint(doc, &best.params, &space, w.oos.0, w.oos.1, &topo, data, env, &binding, &overrides);
|
||||
oos_report.manifest.selection = Some(selection);
|
||||
WindowRun { chosen_params: best.params, oos_equity, oos_report }
|
||||
})
|
||||
match catch_member_panic(|| {
|
||||
run_oos_blueprint(doc, &best.params, &space, w.oos.0, w.oos.1, &topo, data, env, &binding, &overrides)
|
||||
}) {
|
||||
Ok((oos_equity, mut oos_report)) => {
|
||||
oos_report.manifest.selection = Some(selection);
|
||||
WindowRun { chosen_params: best.params, oos_equity, oos_report }
|
||||
}
|
||||
Err(msg) => {
|
||||
window_faults.lock().expect("fault capture lock").push((w, msg));
|
||||
placeholder_window_run(&space)
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Some(msg) = lowest_window_fault(&result, window_faults) {
|
||||
exit_on_member_panic(&msg);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// A fresh seeded synthetic price walk for one Monte-Carlo draw — `blueprint_mc_family`'s
|
||||
@@ -642,11 +786,35 @@ pub fn blueprint_mc_family(
|
||||
// re-runs the shared reduce-mode member path over its own seeded synthetic walk.
|
||||
let seeds: Vec<u64> = (1..=n_seeds).collect();
|
||||
let base_point: Vec<Scalar> = Vec::new();
|
||||
// #278: `run_blueprint_member` ran bare here, the one family builder without
|
||||
// the #272 fault boundary its sweep/walk-forward siblings gained in 51096a3
|
||||
// — a member-compile panic (e.g. an `Sma::new` length assert) would otherwise
|
||||
// unwind straight through `monte_carlo`'s `run_indexed` to an uncaught exit
|
||||
// 101. Contained the same way: `catch_member_panic` + a per-seed capture,
|
||||
// resolved to the LOWEST seed's message after the join (thread-order-
|
||||
// independent, C1) via `lowest_seed_fault`.
|
||||
let faults: Mutex<Vec<(u64, String)>> = Mutex::new(Vec::new());
|
||||
let family = monte_carlo(&base_point, &seeds, |seed, _base| {
|
||||
let sources = synthetic_walk_sources(seed);
|
||||
let window = window_of(&sources).expect("non-empty synthetic walk");
|
||||
run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
|
||||
match catch_member_panic(|| {
|
||||
run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
|
||||
}) {
|
||||
Ok(report) => report,
|
||||
Err(msg) => {
|
||||
faults.lock().expect("fault capture lock").push((seed, msg));
|
||||
axis_grid_probe_report()
|
||||
}
|
||||
}
|
||||
});
|
||||
// The captured fault must win BEFORE the vacuous-mc guard below: a faulted
|
||||
// draw's placeholder report is metrics-identical across every faulted seed,
|
||||
// so a run with >= 2 faulted seeds (or one faulted + one real draw sharing
|
||||
// its placeholder's zero metrics) could otherwise trip the vacuous refusal
|
||||
// instead of surfacing the real member fault.
|
||||
if let Some(msg) = lowest_seed_fault(&family, faults) {
|
||||
exit_on_member_panic(&msg);
|
||||
}
|
||||
// Silent-vacuous MC guard (refuse-don't-guess, C10): with >= 2 seeds, if every draw's
|
||||
// metrics are bit-identical to the first, no seed reached a distinguishable realization —
|
||||
// the strategy never warmed over the fixed synthetic walk (e.g. a lookback as deep as the
|
||||
|
||||
@@ -462,7 +462,7 @@ fn stale_warning(
|
||||
) -> Option<String> {
|
||||
if source_mtime > dylib_mtime {
|
||||
Some(format!(
|
||||
"warning: {} may be stale — dylib built {} but a project source \
|
||||
"aura: warning: {} may be stale — dylib built {} but a project source \
|
||||
file was modified {} (run `cargo build` to refresh); proceeding \
|
||||
with the existing dylib",
|
||||
dylib_path.display(),
|
||||
@@ -916,6 +916,7 @@ mod tests {
|
||||
let source_mtime = UNIX_EPOCH + Duration::from_secs(2_003_702_400); // 2033-06-30
|
||||
let path = PathBuf::from("/tmp/x.so");
|
||||
let msg = stale_warning(&path, dylib_mtime, source_mtime).expect("source is newer");
|
||||
assert!(msg.starts_with("aura: warning: "), "carries the warning class marker (#278): {msg}");
|
||||
assert!(msg.contains("2001"), "names the dylib's mtime: {msg}");
|
||||
assert!(msg.contains("2033"), "names the source's newer mtime: {msg}");
|
||||
}
|
||||
|
||||
@@ -259,6 +259,28 @@ pub fn tap_channel(tap: &str) -> Option<TapChannel> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The note text for a requested tap outside the closed vocabulary (#278):
|
||||
/// pure over just the tap name, so the exact marker-bearing text
|
||||
/// `persist_campaign_traces` writes to stderr is unit-testable without
|
||||
/// booting a real archive run (the `tap_channel` `None` arm that reaches
|
||||
/// this is defensive — `validate_campaign` already refuses an unknown tap
|
||||
/// name before this function ever runs).
|
||||
fn tap_not_produced_note(tap: &str) -> String {
|
||||
format!("aura: note: tap \"{tap}\" is not produced by this run; skipped")
|
||||
}
|
||||
|
||||
/// The note text for a cell with neither a nominee nor a non-empty terminal
|
||||
/// family (#278): pure over the record's own cell fields, so the exact
|
||||
/// marker-bearing text `persist_campaign_traces` writes to stderr is
|
||||
/// unit-testable without booting a real archive run — the `cell_member_fanout`
|
||||
/// twin above already covers the emptiness logic itself.
|
||||
fn no_nominee_note(strategy: &str, instrument: &str, window_ms: (i64, i64)) -> String {
|
||||
format!(
|
||||
"aura: note: cell {strategy}/{instrument}/[{}, {}]: no nominee; no traces persisted",
|
||||
window_ms.0, window_ms.1
|
||||
)
|
||||
}
|
||||
|
||||
/// The content-derived on-disk key of one campaign cell under
|
||||
/// `traces/<trace_name>/` — the `member_key` discipline (content, never a
|
||||
/// runtime ordinal): strategy content-id prefix + instrument +
|
||||
@@ -368,10 +390,10 @@ pub fn persist_campaign_traces(
|
||||
for tap in taps {
|
||||
match tap_channel(tap) {
|
||||
Some(TapChannel::Net) if campaign.cost.is_empty() => eprintln!(
|
||||
"aura: tap \"{tap}\" needs a cost model; add a cost block to the campaign document; skipped"
|
||||
"aura: note: tap \"{tap}\" needs a cost model; add a cost block to the campaign document; skipped"
|
||||
),
|
||||
Some(ch) => routed.push((tap.as_str(), ch)),
|
||||
None => eprintln!("aura: tap \"{tap}\" is not produced by this run; skipped"),
|
||||
None => eprintln!("{}", tap_not_produced_note(tap)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,8 +410,8 @@ pub fn persist_campaign_traces(
|
||||
let members = cell_member_fanout(cell_out);
|
||||
if members.is_empty() {
|
||||
eprintln!(
|
||||
"aura: cell {}/{}/[{}, {}]: no nominee; no traces persisted",
|
||||
cell_rec.strategy, cell_rec.instrument, cell_rec.window_ms.0, cell_rec.window_ms.1
|
||||
"{}",
|
||||
no_nominee_note(&cell_rec.strategy, &cell_rec.instrument, cell_rec.window_ms)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -753,4 +775,27 @@ mod tests {
|
||||
assert_eq!(tap_channel("net_r_equity"), Some(TapChannel::Net));
|
||||
assert_eq!(tap_channel("bogus"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// #278: the unrecognized-tap note carries the `note` class marker AND
|
||||
/// names the offending tap — the exact text `persist_campaign_traces`
|
||||
/// writes to stderr on `tap_channel`'s defensive `None` arm.
|
||||
fn tap_not_produced_note_carries_the_note_marker_and_names_the_tap() {
|
||||
let msg = tap_not_produced_note("bogus");
|
||||
assert!(msg.starts_with("aura: note: "), "carries the note class marker: {msg}");
|
||||
assert!(msg.contains("\"bogus\""), "names the offending tap: {msg}");
|
||||
assert!(msg.contains("is not produced by this run"), "states the reason: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// #278: the no-candidate-cell note carries the `note` class marker AND
|
||||
/// names the cell (strategy/instrument/window) — the exact text
|
||||
/// `persist_campaign_traces` writes to stderr when `cell_member_fanout`
|
||||
/// (unit-tested above) reports no nominee and no non-empty family.
|
||||
fn no_nominee_note_carries_the_note_marker_and_names_the_cell() {
|
||||
let msg = no_nominee_note("bb34aa55", "GER40", (1_000, 2_000));
|
||||
assert!(msg.starts_with("aura: note: "), "carries the note class marker: {msg}");
|
||||
assert!(msg.contains("bb34aa55/GER40/[1000, 2000]"), "names the cell: {msg}");
|
||||
assert!(msg.contains("no nominee; no traces persisted"), "states the reason: {msg}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@ fn the_shell_defines_no_domain_modules_and_no_lib_target() {
|
||||
modules,
|
||||
[
|
||||
"campaign_run.rs",
|
||||
"diag.rs",
|
||||
"graph_construct.rs",
|
||||
"main.rs",
|
||||
"render.rs",
|
||||
|
||||
@@ -31,6 +31,20 @@ frozen deploy artifact ([invariant 8](c13-hot-reload-frozen-deploy.md)) cannot
|
||||
pick up. Usage lines follow the clap house style (`Usage: aura <verb> …`);
|
||||
refusal diagnostics stay unprefixed, since a diagnostic is not a usage line.
|
||||
|
||||
**The stderr class markers (#278/#313).** Diagnostics on a *continuing* run
|
||||
carry a stable class marker the exit code cannot supply: `aura: note: <text>`
|
||||
for a benign, expected diagnostic — a gate-emptied cell, a skipped tap, an
|
||||
all-zero-trade walk-forward — where the run stays valid and the exit code is
|
||||
unaffected (a null result is a valid research result, #198); and
|
||||
`aura: warning: <text>` for a recorded fault or suspect condition the run
|
||||
survives — a failed cell feeding exit 3, a failed scaffold git step, a stale
|
||||
hot-reload dylib. Error lines that accompany a non-zero exit — and plain
|
||||
info lines such as the run-record summary — keep the bare `aura: ` prefix;
|
||||
for errors, the exit-code partition below is the machine contract.
|
||||
The markers are grep-stable: `grep '^aura: warning: '` isolates faults,
|
||||
`grep '^aura: note: '` benign notices. The grammar's single source is
|
||||
`aura-cli/src/diag.rs`; the few runner-side literals repeat it verbatim.
|
||||
|
||||
**The exit-code partition** is a durable part of the automation contract: a
|
||||
caller branches on the failure class without parsing stderr.
|
||||
|
||||
|
||||
@@ -305,6 +305,10 @@ System Quality Number — `√n · mean_R / stdev_R`: the dispersion- and trade-
|
||||
**Avoid:** — (superseded)
|
||||
**Superseded (2026-06-28 C10 reframe): there is no Stage-2 currency/compounding gate.** Research is one pure feed-forward R loop — gross R → net R via the `cost model` — with money, compounding, and a real broker living only at a separate later **live / deploy edge** (compounding is a post-hoc money-management transform of the net-R sequence, not an in-loop axis). The pre-reframe cleave gated a Stage-2 currency / fixed-fractional / realistic-broker layer behind `E[R] > 0`; **"Stage-1"** now survives only as a historical cycle name — the shipped feed-forward R chain's identifier family was renamed to the r-family (`r-sma` / `r-breakout` / `r-meanrev`, #174) — not one half of a two-stage gate.
|
||||
|
||||
### stderr class marker
|
||||
**Avoid:** log level, severity tag
|
||||
The stable prefix pair separating diagnostics on a continuing run: `aura: note: ` marks a benign, expected outcome (exit code unaffected), `aura: warning: ` a recorded fault or suspect condition the run survives. Error lines that accompany a non-zero exit and plain info lines keep the bare `aura: ` prefix — for errors, the exit-code partition (C14) is the machine contract.
|
||||
|
||||
### strategy
|
||||
**Avoid:** —
|
||||
A reusable composite-node blueprint — broker-, data-, and viz-independent, inputs declared as named roles — whose output is the **bias** stream (unsized direction + conviction; not an equity curve, not a size; sizing and the position table are derived downstream layers). Frozen with a broker into a bot for deploy.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
### run A: zero-trade walkforward (synthetic) — expect note, exit 0
|
||||
aura: note: all 3 walk-forward windows recorded zero trades
|
||||
# exitA=0
|
||||
### run B: real sweep with a poison axis member — expect warning, exit 3
|
||||
# exitB=3
|
||||
aura: warning: cell (df772d885a1b9c6b46a6ea51ba1339f0f3ee5c22e47ac4a3c95867ebc9b3b9d2, EURUSD, [1577923200000, 1609451819999]) failed at stage 0: a member panicked: SMA length must be >= 1 — recorded, campaign continues
|
||||
aura: warning: cell (df772d885a1b9c6b46a6ea51ba1339f0f3ee5c22e47ac4a3c95867ebc9b3b9d2, EURUSD, [1577923200000, 1609451819999]) failed at stage 0: a member panicked: SMA length must be >= 1 — recorded, campaign continues
|
||||
@@ -0,0 +1,2 @@
|
||||
aura: warning: cell (df772d885a1b9c6b46a6ea51ba1339f0f3ee5c22e47ac4a3c95867ebc9b3b9d2, EURUSD, [1577923200000, 1609451819999]) failed at stage 0: a member panicked: SMA length must be >= 1 — recorded, campaign continues
|
||||
aura: warning: cell (df772d885a1b9c6b46a6ea51ba1339f0f3ee5c22e47ac4a3c95867ebc9b3b9d2, EURUSD, [1577923200000, 1609451819999]) failed at stage 0: a member panicked: SMA length must be >= 1 — recorded, campaign continues
|
||||
@@ -0,0 +1 @@
|
||||
aura: note: all 3 walk-forward windows recorded zero trades
|
||||
@@ -0,0 +1 @@
|
||||
aura: note: the single walk-forward window recorded zero trades
|
||||
@@ -0,0 +1,34 @@
|
||||
### real sweep, poison member (CAUGHT -> warning?, survives?)
|
||||
# exit=3
|
||||
aura: warning: cell (df772d885a1b9c6b46a6ea51ba1339f0f3ee5c22e47ac4a3c95867ebc9b3b9d2, EURUSD, [1577923200000, 1609451819999]) failed at stage 0: a member panicked: SMA length must be >= 1 — recorded, campaign continues
|
||||
|
||||
### synthetic walkforward, SAME poison (uncaught panic?)
|
||||
# exit=3
|
||||
aura: warning: a member panicked: SMA length must be >= 1
|
||||
|
||||
### sweep, every member zero trades (any notice?)
|
||||
# exit=0
|
||||
|
||||
### mc, vacuous / all seeds identical (marker? exit?)
|
||||
# exit=2
|
||||
aura: mc is vacuous: every seed produced an identical result — the strategy never warmed over the synthetic walk, so no seed reached a distinguishable realization; use a shallower-lookback blueprint or a longer walk
|
||||
|
||||
### generalize, every instrument zero trades (any notice?)
|
||||
# exit=0
|
||||
|
||||
### generalize, one instrument has no data in window (marker? exit?)
|
||||
# exit=1
|
||||
aura: generalize produced no cross-instrument grade
|
||||
|
||||
### walkforward yielding a single OOS window (plural grammar)
|
||||
# exit=0
|
||||
aura: note: the single walk-forward window recorded zero trades
|
||||
|
||||
### walkforward without a project (real mode)
|
||||
# exit=1
|
||||
aura: walkforward needs a project: strategies resolve against the project store and vocabulary (no Aura.toml found up from /tmp/tmp.MQAgiiVtay)
|
||||
|
||||
### degenerate axis on a NON-campaign call (graph build finalize)
|
||||
# exit=1
|
||||
aura: finalize: slot const.clock is unconnected
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Example 1 — Mission (1): a consumer separates benign notices from real faults
|
||||
# by FORM alone (grep / CI log scan), and checks exit codes against the binary's
|
||||
# own conventions.
|
||||
#
|
||||
# Finding proven here: the two markers cannot co-occur in ONE invocation (the
|
||||
# only note-emitting verb, walkforward, is single-cell all-or-nothing: the note
|
||||
# fires only when every window SUCCEEDS with zero trades, i.e. exit 0; any
|
||||
# warning implies a cell/fold failure, i.e. exit 3). So separability is shown
|
||||
# the way the promise's "CI log scan" actually consumes it: over the accumulated
|
||||
# stderr of several runs.
|
||||
set -u
|
||||
AURA="${AURA:?set AURA to the release binary path}"
|
||||
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||
LOG="$HERE/captured/example1_combined.log"
|
||||
|
||||
# Build the synthetic zero-trade strategy from its canonical op-list.
|
||||
"$AURA" graph build < "$HERE/ops/flat.ops.json" > "$WORK/flat.json"
|
||||
|
||||
# A real project is required to reach the `aura: warning:` class at all
|
||||
# (synthetic member panics are uncaught — see example 3). Scaffold one.
|
||||
( cd "$WORK" && "$AURA" new proj >/dev/null )
|
||||
|
||||
: > "$LOG"
|
||||
echo "### run A: zero-trade walkforward (synthetic) — expect note, exit 0" >> "$LOG"
|
||||
"$AURA" walkforward "$WORK/flat.json" --axis graph.sma.length=2,3,5 >/dev/null 2>>"$LOG"
|
||||
echo "# exitA=$?" >> "$LOG"
|
||||
echo "### run B: real sweep with a poison axis member — expect warning, exit 3" >> "$LOG"
|
||||
( cd "$WORK/proj" && "$AURA" sweep blueprints/signal.json --real EURUSD \
|
||||
--from 1577836800000 --to 1609459200000 --axis proj_signal.fast.length=0,2 \
|
||||
>/dev/null 2>>"$HERE/captured/example1_runB.err" )
|
||||
echo "# exitB=$?" >> "$LOG"
|
||||
cat "$HERE/captured/example1_runB.err" >> "$LOG"
|
||||
|
||||
echo "=== combined log ==="; cat "$LOG"
|
||||
echo "=== benign notices (grep '^aura: note: ') ==="; grep '^aura: note: ' "$LOG" || true
|
||||
echo "=== real faults (grep '^aura: warning: ') ==="; grep '^aura: warning: ' "$LOG" || true
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Example 2 — Mission (2): a walk-forward whose every window records zero trades
|
||||
# must announce itself with an explicit notice; one that trades must not; exit
|
||||
# code stays success either way. Blueprints authored via the binary's own
|
||||
# op-script surface (`aura graph build`).
|
||||
set -u
|
||||
AURA="${AURA:?set AURA to the release binary path}"
|
||||
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
"$AURA" graph build < "$HERE/ops/flat.ops.json" > "$WORK/flat.json"
|
||||
"$AURA" graph build < "$HERE/ops/momentum.ops.json" > "$WORK/momentum.json"
|
||||
cd "$WORK" # synthetic families record to ./runs — keep them in the temp dir
|
||||
|
||||
echo "=== A: flat (zero-trade) walkforward — expect note + exit 0 ==="
|
||||
"$AURA" walkforward "$WORK/flat.json" --axis graph.sma.length=2,3,5 \
|
||||
>/dev/null 2>"$HERE/captured/example2_flat.err"
|
||||
echo "# exit=$?"; cat "$HERE/captured/example2_flat.err"
|
||||
|
||||
echo "=== B: momentum (trades) walkforward — expect NO note + exit 0 ==="
|
||||
"$AURA" walkforward "$WORK/momentum.json" --axis graph.sma.length=2,3,5 \
|
||||
>/dev/null 2>"$HERE/captured/example2_momentum.err"
|
||||
echo "# exit=$?"; cat "$HERE/captured/example2_momentum.err"; echo "(stderr empty above = no note)"
|
||||
|
||||
echo "=== C: single-window edge — expect the 'all 1 windows' plural nit ==="
|
||||
# The synthetic roller always yields 3 windows; a sub-120-day REAL window yields
|
||||
# exactly one OOS window, which exposes the ungrammatical "all 1 ... windows".
|
||||
( cd "$WORK" && "$AURA" new proj >/dev/null )
|
||||
( cd "$WORK/proj" && "$AURA" walkforward blueprints/signal.json --real EURUSD \
|
||||
--from 1577836800000 --to 1583020800000 --axis proj_signal.fast.length=4 \
|
||||
>/dev/null 2>"$HERE/captured/example2_singular.err" )
|
||||
echo "# exit=$?"; cat "$HERE/captured/example2_singular.err"
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# Example 3 — Mission (3): from the consumer's seat, do any OTHER diagnostics
|
||||
# encountered en route fall OUTSIDE both stderr classes (note/warning) when they
|
||||
# plausibly belong to one? Survey the diagnostics a driver actually hits and tag
|
||||
# each with (marker?, exit code, verdict). Reads only the binary's own output.
|
||||
set -u
|
||||
AURA="${AURA:?set AURA to the release binary path}"
|
||||
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
|
||||
OUT="$HERE/captured/example3_survey.log"
|
||||
: > "$OUT"
|
||||
|
||||
run() { # label ; then the command via "$@"
|
||||
local label="$1"; shift
|
||||
"$@" >/dev/null 2>"$WORK/e"; local e=$?
|
||||
{ echo "### $label"; echo "# exit=$e"; sed 's/^/ /' "$WORK/e"; echo; } >> "$OUT"
|
||||
}
|
||||
|
||||
"$AURA" graph build < "$HERE/ops/flat.ops.json" > "$WORK/flat.json"
|
||||
"$AURA" graph build < "$HERE/ops/momentum.ops.json" > "$WORK/momentum.json"
|
||||
"$AURA" graph build < "$HERE/ops/flat_closed.ops.json" > "$WORK/flat_closed.json"
|
||||
( cd "$WORK" && "$AURA" new proj >/dev/null )
|
||||
|
||||
# (A) SAME fault class, two surfaces: real campaign vs synthetic.
|
||||
run "real sweep, poison member (CAUGHT -> warning?, survives?)" \
|
||||
bash -c "cd '$WORK/proj' && '$AURA' sweep blueprints/signal.json --real EURUSD --from 1577836800000 --to 1609459200000 --axis proj_signal.fast.length=0,2"
|
||||
run "synthetic walkforward, SAME poison (uncaught panic?)" \
|
||||
"$AURA" walkforward "$WORK/momentum.json" --axis graph.sma.length=0,3
|
||||
|
||||
# (B) null-result siblings of the zero-trade note that get NO note.
|
||||
run "sweep, every member zero trades (any notice?)" \
|
||||
"$AURA" sweep "$WORK/flat.json" --axis graph.sma.length=2,3,5
|
||||
run "mc, vacuous / all seeds identical (marker? exit?)" \
|
||||
"$AURA" mc "$WORK/flat_closed.json" --seeds 5
|
||||
run "generalize, every instrument zero trades (any notice?)" \
|
||||
bash -c "cd '$WORK/proj' && '$AURA' generalize blueprints/signal.json --real EURUSD,GBPUSD --from 1577836800000 --to 1609459200000 --axis proj_signal.fast.length=4"
|
||||
|
||||
# (C) partial-failure surfaced without a marker.
|
||||
run "generalize, one instrument has no data in window (marker? exit?)" \
|
||||
bash -c "cd '$WORK/proj' && '$AURA' generalize blueprints/signal.json --real EURUSD,Copper --from 1514764800000 --to 1530403200000 --axis proj_signal.fast.length=4"
|
||||
|
||||
# (D) plural-agreement nit on the note itself.
|
||||
run "walkforward yielding a single OOS window (plural grammar)" \
|
||||
bash -c "cd '$WORK/proj' && '$AURA' walkforward blueprints/signal.json --real EURUSD --from 1577836800000 --to 1583020800000 --axis proj_signal.fast.length=4"
|
||||
|
||||
# (E) hard errors that (correctly) sit outside both classes.
|
||||
run "walkforward without a project (real mode)" \
|
||||
bash -c "cd '$WORK' && '$AURA' walkforward proj/blueprints/signal.json --real EURUSD --axis proj_signal.fast.length=2"
|
||||
run "degenerate axis on a NON-campaign call (graph build finalize)" \
|
||||
bash -c "echo '[{\"op\":\"add\",\"type\":\"Const\"}]' | '$AURA' graph build"
|
||||
|
||||
cat "$OUT"
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{"op":"doc","text":"flat: bias = 0 * sign(close - SMA(close,length)); Scale factor bound to 0 so no window ever trades, while sma.length stays an open axis"},
|
||||
{"op":"source","role":"close","kind":"F64"},
|
||||
{"op":"add","type":"SMA","name":"sma"},
|
||||
{"op":"add","type":"Sub","name":"d"},
|
||||
{"op":"add","type":"Sign","name":"s"},
|
||||
{"op":"add","type":"Scale","name":"z","bind":{"factor":{"F64":0.0}}},
|
||||
{"op":"feed","role":"close","into":["sma.series","d.lhs"]},
|
||||
{"op":"connect","from":"sma.value","to":"d.rhs"},
|
||||
{"op":"connect","from":"d.value","to":"s.value"},
|
||||
{"op":"connect","from":"s.value","to":"z.signal"},
|
||||
{"op":"expose","from":"z.value","as":"bias"}
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{"op":"doc","text":"flat-closed: bias = 0, all params bound (mc requires a closed blueprint)"},
|
||||
{"op":"source","role":"close","kind":"F64"},
|
||||
{"op":"add","type":"SMA","name":"sma","bind":{"length":{"I64":3}}},
|
||||
{"op":"add","type":"Sub","name":"d"},
|
||||
{"op":"add","type":"Sign","name":"s"},
|
||||
{"op":"add","type":"Scale","name":"z","bind":{"factor":{"F64":0.0}}},
|
||||
{"op":"feed","role":"close","into":["sma.series","d.lhs"]},
|
||||
{"op":"connect","from":"sma.value","to":"d.rhs"},
|
||||
{"op":"connect","from":"d.value","to":"s.value"},
|
||||
{"op":"connect","from":"s.value","to":"z.signal"},
|
||||
{"op":"expose","from":"z.value","as":"bias"}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{"op":"doc","text":"momentum: bias = sign(close - SMA(close,length)); length is the open IS-refit axis"},
|
||||
{"op":"source","role":"close","kind":"F64"},
|
||||
{"op":"add","type":"SMA","name":"sma"},
|
||||
{"op":"add","type":"Sub","name":"d"},
|
||||
{"op":"add","type":"Sign","name":"s"},
|
||||
{"op":"feed","role":"close","into":["sma.series","d.lhs"]},
|
||||
{"op":"connect","from":"sma.value","to":"d.rhs"},
|
||||
{"op":"connect","from":"d.value","to":"s.value"},
|
||||
{"op":"expose","from":"s.value","as":"bias"}
|
||||
]
|
||||
Reference in New Issue
Block a user