feat: per-instrument pip metadata channel for the sim-optimal broker
The data-server symbol stream carries only price bars, no instrument metadata.
So SimBroker was constructed with one global pip_size literal (0.0001, correct
only for 5-decimal FX), and `aura run --real <symbol>` emitted wrong-unit pip
equity for any non-FX instrument (AAPL.US, BTCUSD off by orders of magnitude).
Add the missing channel to *specify* per-instrument pip:
- aura-ingest: `InstrumentSpec { pip_size }` + `instrument_spec(symbol)` — a
typed, Rust-authored vetted lookup (NOT Aura.toml; the later Aura.toml schema
can subsume it). Seeded GER40/FRA40 = 1.0 (index points), EURUSD/GBPUSD/USDCAD
= 0.0001 (5-dp FX majors); None for an un-specced symbol.
- aura-cli: `sample_harness` and `sim_optimal_manifest` gain a `pip_size`
parameter; `run_sample_real` looks the pip up by symbol BEFORE any data access
and refuses (exit 2) an un-specced instrument rather than guessing — honest by
construction. The looked-up pip reaches both the broker divisor and the
manifest broker label (`format!`). Every synthetic caller passes the unchanged
0.0001, now named `SYNTHETIC_PIP_SIZE`. SimBroker (aura-std) is untouched.
Scope: narrowed to the CLI real path — the actual bug. The runnable GER40
examples already bake the correct 1.0 and are NOT buggy; centralizing them
through the lookup is deferred to #98.
Decisions (user-directed, recorded on #22): metadata channel = typed
InstrumentSpec at the source edge; un-specced real symbol refuses; seed =
GER40/FRA40 + FX majors; example refactor deferred. The spec went through the
grounding-check gate; the auto-sign panel surfaced the refusal-behaviour and
scope as genuine design decisions, escalated and resolved by the user.
Tests: instrument_spec unit; manifest pip rendering (1.0 -> "1", 0.0001
unchanged); non-gated CLI refusal (exit 2, no stdout leak, vetted symbol clears
the lookup); gated GER40 binary-boundary honesty (pip_size=1 in the emitted
manifest, deterministic). The pre-existing run_sample_real determinism test was
retargeted AAPL.US -> GER40 (AAPL.US is un-specced and would now refuse at the
lookup); the AAPL.US bounded-window streaming property still lives in
aura-ingest/tests/streaming_seam.rs (literal source, no spec lookup).
Verified: cargo build --workspace, clippy --all-targets -D warnings, and
cargo test --workspace all green; the gated GER40 path ran with local data.
closes #22
This commit is contained in:
@@ -130,6 +130,124 @@ fn run_with_trailing_token_is_strict_and_exits_two() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_real_unspecced_symbol_refuses_with_exit_2() {
|
||||
// The instrument_spec lookup precedes any data access, so an un-specced symbol
|
||||
// refuses with NO local data required (CI-safe). "NONEXISTENT" is not in the
|
||||
// vetted table.
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["run", "--real", "NONEXISTENT"])
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "expected exit 2");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("no vetted pip/instrument spec for symbol 'NONEXISTENT'"),
|
||||
"expected the per-instrument-pip refusal message, got: {stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property: the un-specced-symbol refusal is a CLEAN refusal — it emits NOTHING
|
||||
/// on stdout, never a partial `RunReport`. The #22 acceptance is "refuse INSTEAD
|
||||
/// of emitting nonsense": the pip lookup precedes harness construction and data
|
||||
/// access, so a `None` spec must short-circuit to `exit(2)` before any JSON line
|
||||
/// can reach stdout. A regression that moved the lookup after a partial run would
|
||||
/// leak a `{"manifest":...}` line here while still exiting non-zero; this pins
|
||||
/// that it cannot. Archive-independent (the lookup never touches local data).
|
||||
#[test]
|
||||
fn run_real_unspecced_symbol_emits_no_stdout_report() {
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["run", "--real", "NONEXISTENT"])
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(2), "expected exit 2: {:?}", out.status);
|
||||
assert!(
|
||||
out.stdout.is_empty(),
|
||||
"the refusal must not leak a partial report to stdout, got: {:?}",
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
);
|
||||
}
|
||||
|
||||
/// Property: the pip-honesty refusal keys on the SYMBOL being un-specced, not on
|
||||
/// `--real` being taken at all. A *vetted* symbol (`EURUSD`, in the instrument
|
||||
/// table) gets PAST the `instrument_spec` lookup, so it never produces the
|
||||
/// pip-refusal message — it either runs to a report (local data present) or hits
|
||||
/// the DISTINCT no-local-data usage path ("no local data for symbol"). Either
|
||||
/// way the "no vetted pip/instrument spec" string is absent. This distinguishes
|
||||
/// the per-instrument-pip honesty lever from the unrelated arg-grammar / no-data
|
||||
/// error paths, and is archive-independent (asserts only on the absence of the
|
||||
/// pip-refusal string, true whether or not EURUSD data is on the machine).
|
||||
#[test]
|
||||
fn run_real_vetted_symbol_never_hits_the_pip_refusal() {
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["run", "--real", "EURUSD"])
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
!stderr.contains("no vetted pip/instrument spec"),
|
||||
"a vetted symbol must clear the pip lookup, never the pip-refusal; stderr: {stderr}"
|
||||
);
|
||||
// It resolves to exactly one of the two legitimate outcomes: a clean report
|
||||
// (exit 0, data present) or the distinct no-local-data refusal (exit 2).
|
||||
match out.status.code() {
|
||||
Some(0) => assert!(
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
.trim_start()
|
||||
.starts_with("{\"manifest\":"),
|
||||
"exit 0 must carry a JSON report, got: {:?}",
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
),
|
||||
Some(2) => assert!(
|
||||
stderr.contains("no local data for symbol 'EURUSD'"),
|
||||
"exit 2 for a vetted symbol must be the no-data path, got: {stderr}"
|
||||
),
|
||||
other => panic!("unexpected exit for a vetted real run: {other:?}; stderr: {stderr}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Property: a vetted symbol threads its looked-up pip all the way to the binary's
|
||||
/// emitted manifest — `aura run --real GER40` renders the INDEX pip
|
||||
/// `sim-optimal(pip_size=1)`, not the FX `0.0001` literal the synthetic path uses.
|
||||
/// This is the #22 headline at the built-binary boundary (criterion 2: GER40=1.0
|
||||
/// vs EURUSD=0.0001 vs un-specced→refuse): the metadata channel reaches stdout, so
|
||||
/// equity is honestly scaled per instrument. Gated on local GER40 data — skip
|
||||
/// cleanly when the archive is absent (the project's skip-on-no-data convention),
|
||||
/// so it never fails on a data-less machine; the no-data path is the distinct
|
||||
/// "no local data" refusal (exit 2), not the pip-refusal, asserted above.
|
||||
#[test]
|
||||
fn run_real_vetted_index_pip_reaches_the_emitted_manifest() {
|
||||
// The vetted GER40 Sept-2024 window (inclusive Unix-ms), the same calendar
|
||||
// month the gated ingest path drives; bounding it keeps the run fast.
|
||||
const FROM_MS: &str = "1725148800000";
|
||||
const TO_MS: &str = "1727740799999";
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["run", "--real", "GER40", "--from", FROM_MS, "--to", TO_MS])
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
|
||||
// Skip on a data-less machine: a vetted symbol with no local data takes the
|
||||
// distinct no-data path (exit 2, "no local data"), never the pip-refusal.
|
||||
if out.status.code() == Some(2) {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("no local data for symbol 'GER40'"),
|
||||
"exit 2 for vetted GER40 must be the no-data path, got: {stderr}"
|
||||
);
|
||||
eprintln!("skip: no local GER40 data");
|
||||
return;
|
||||
}
|
||||
|
||||
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
|
||||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||||
let line = stdout.trim_end();
|
||||
// the looked-up index pip (1.0 -> "1") reaches the manifest, not the FX 0.0001.
|
||||
assert!(
|
||||
line.contains("\"broker\":\"sim-optimal(pip_size=1)\""),
|
||||
"GER40 must render the index pip in the manifest, got: {line}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_args_prints_usage_and_exits_two() {
|
||||
let out = Command::new(BIN).output().expect("spawn aura");
|
||||
|
||||
Reference in New Issue
Block a user