From 7e4b91ab5f317f722e2a4a272bafb9da6580240d Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 25 Jun 2026 19:06:12 +0200 Subject: [PATCH] feat(0074): source real-path pip from the geometry sidecar; drop the authored floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The data-server now ships per-symbol geometry sidecars, so aura no longer hard-codes instrument geometry. A consumption grep showed production reads exactly one `InstrumentSpec` field — `pip_size` — at the real-data path; the other eight (incl. `instrument_id`) were constructed but never read. - aura-cli resolves the real-path pip from the recorded sidecar (`instrument_geometry` -> `InstrumentGeometry.pip_size`), refusing (exit 2) on absent geometry. The authored table no longer gates: any symbol with a sidecar + bars now runs (new test: USDJPY, never vetted, resolves its JPY pip 0.01 from the sidecar). - Removed the redundant authored floor: `InstrumentSpec`, `instrument_spec`, `VETTED_SYMBOLS`, and the table-only tests. The `InstrumentGeometry` seam (the surviving pip source) and its nameability guard are kept. - `instrument_id` (the "find out"): no production consumer. The position-event table's `instrument_id` is a decoupled caller parameter; the engine never imports an instrument spec. Dropped with the struct. - The example `pip_size_of` is re-sourced to the sidecar; the pure-structural blueprint tests use a literal pip to stay archive-independent. Speculative deploy-grade fields and the #124 override/floor tiers are deferred to the Stage-2 broker that will consume them, read from the sidecar geometry then. Ledger (C8/C15/#22/#106) updated; #124 collapses to "recorded sidecar geometry -> refuse" in the meantime. Verified: cargo test --workspace (all green), clippy -D warnings clean, cargo doc clean, with the local archive present so the sidecar paths run. refs #124 --- crates/aura-cli/src/main.rs | 50 +++-- crates/aura-cli/tests/cli_run.rs | 149 ++++++++++---- crates/aura-engine/src/report.rs | 6 +- .../examples/ger40_breakout_compare.rs | 6 +- .../examples/shared/breakout_real.rs | 12 +- crates/aura-ingest/src/lib.rs | 188 +----------------- .../tests/ger40_breakout_blueprint.rs | 8 +- .../aura-ingest/tests/instrument_geometry.rs | 12 ++ .../tests/instrument_geometry_crosscheck.rs | 82 -------- .../tests/instrument_spec_deploy_metadata.rs | 93 --------- docs/design/INDEX.md | 56 +++--- 11 files changed, 198 insertions(+), 464 deletions(-) create mode 100644 crates/aura-ingest/tests/instrument_geometry.rs delete mode 100644 crates/aura-ingest/tests/instrument_geometry_crosscheck.rs delete mode 100644 crates/aura-ingest/tests/instrument_spec_deploy_metadata.rs diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 05cab93..c816b44 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -38,10 +38,10 @@ use std::collections::HashSet; /// The pip size the built-in *synthetic* harnesses run at: a 5-decimal FX major /// (`EURUSD`-shaped). The synthetic streams carry no instrument, so there is no -/// `instrument_spec` to thread; a single named source keeps the broker's divisor -/// (`sample_harness` / `SimBroker::builder`) and the recorded broker label +/// recorded geometry sidecar to thread; a single named source keeps the broker's +/// divisor (`sample_harness` / `SimBroker::builder`) and the recorded broker label /// (`sim_optimal_manifest`) in lockstep, so they cannot silently drift apart. -/// The real path threads the looked-up `spec.pip_size` instead. +/// The real path threads the sidecar's looked-up `pip_size` instead. const SYNTHETIC_PIP_SIZE: f64 = 0.0001; /// Real walk-forward roller sizes (Fork D/F). `WindowRoller` takes sizes in the @@ -723,15 +723,21 @@ fn no_real_data(symbol: &str) -> ! { std::process::exit(2) } -/// Look up the vetted per-instrument pip/spec, or refuse (stderr + exit 2) on an -/// un-specced symbol — the single home of the guessed-pip refusal message, shared -/// by `run_sample_real` and `DataSource::from_choice`. Refusing BEFORE any data -/// access keeps the pip honest by construction (never guessed). -fn instrument_spec_or_refuse(symbol: &str) -> aura_ingest::InstrumentSpec { - match aura_ingest::instrument_spec(symbol) { - Some(s) => s, +/// Resolve the per-instrument pip from the recorded geometry sidecar, or refuse +/// (stderr + exit 2) when the symbol has no recorded geometry — the single home of +/// the guessed-pip refusal, shared by `open_real_source` and `from_choice`. Reads +/// the symbol's geometry metadata (not bar data) before the run source opens, so an +/// instrument with no recorded geometry refuses without a guessed pip; the pip is +/// the provider's recorded value, honest by construction. +fn pip_or_refuse(server: &std::sync::Arc, symbol: &str) -> f64 { + match aura_ingest::instrument_geometry(server, symbol) { + Some(geo) => geo.pip_size, None => { - eprintln!("aura: no vetted pip/instrument spec for symbol '{symbol}' — refusing to run a real instrument with a guessed pip (add it to the instrument table)"); + eprintln!( + "aura: no recorded geometry for symbol '{symbol}' at {} — \ + refusing to run a real instrument with a guessed pip", + data_server::DEFAULT_DATA_PATH + ); std::process::exit(2); } } @@ -758,10 +764,10 @@ fn probe_window( (first, last) } -/// Open a real M1-close source for a vetted symbol over an optional window, returning +/// Open a real M1-close source for a recorded symbol over an optional window, returning /// the run source paired with its manifest `window` and per-instrument `pip_size`. /// Single home of the real-source construction the single-run handlers share — the -/// vetted-pip lookup, the `DataServer` `has_symbol` refusal, the probe-window pass, and +/// sidecar-pip lookup, the `DataServer` `has_symbol` refusal, the probe-window pass, and /// the run-source `open` (each refusal an stderr + exit 2). Pre-data refusals keep the /// pip honest by construction. Shared by `run_sample_real` and `run_stage1_r`. fn open_real_source( @@ -769,10 +775,10 @@ fn open_real_source( from_ms: Option, to_ms: Option, ) -> (Box, (Timestamp, Timestamp), f64) { - // Per-instrument pip: look up BEFORE any data access, so an un-specced symbol - // refuses without touching local data. Honest by construction. - let spec = instrument_spec_or_refuse(symbol); + // Per-instrument pip from the recorded sidecar; resolved BEFORE bar-data access + // so an instrument with no geometry refuses without touching the archive. let server = std::sync::Arc::new(data_server::DataServer::new(data_server::DEFAULT_DATA_PATH)); + let pip = pip_or_refuse(&server, symbol); if !server.has_symbol(symbol) { no_real_data(symbol); } @@ -783,24 +789,24 @@ fn open_real_source( Some(s) => Box::new(s), None => no_real_data(symbol), }; - (source, window, spec.pip_size) + (source, window, pip) } impl DataSource { - /// Build a provider from a parsed choice, or refuse (stderr + exit 2) on an - /// un-vetted symbol / absent data — both BEFORE any member runs (Fork C/G), - /// via the same `instrument_spec_or_refuse` / `no_real_data` helpers as + /// Build a provider from a parsed choice, or refuse (stderr + exit 2) on a symbol + /// with no recorded geometry / absent data — both BEFORE any member runs (Fork C/G), + /// via the same `pip_or_refuse` / `no_real_data` helpers as /// `run_sample_real`. fn from_choice(choice: DataChoice) -> DataSource { match choice { DataChoice::Synthetic => DataSource::Synthetic, DataChoice::Real { symbol, from_ms, to_ms } => { - let spec = instrument_spec_or_refuse(&symbol); let server = std::sync::Arc::new(data_server::DataServer::new(data_server::DEFAULT_DATA_PATH)); + let pip = pip_or_refuse(&server, &symbol); if !server.has_symbol(&symbol) { no_real_data(&symbol); } - DataSource::Real { server, symbol, from_ms, to_ms, pip: spec.pip_size } + DataSource::Real { server, symbol, from_ms, to_ms, pip } } } } diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index 35ea2a3..acabfe9 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -131,10 +131,10 @@ 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. +fn run_real_no_geometry_symbol_refuses_with_exit_2() { + // The geometry-sidecar lookup precedes any bar-data access, so a symbol with no + // recorded geometry refuses with NO local data required (CI-safe). "NONEXISTENT" + // has no recorded geometry on any host. let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args(["run", "--real", "NONEXISTENT"]) .output() @@ -142,11 +142,43 @@ fn run_real_unspecced_symbol_refuses_with_exit_2() { 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'"), + stderr.contains("no recorded geometry for symbol 'NONEXISTENT'"), "expected the per-instrument-pip refusal message, got: {stderr}" ); } +/// Property: the pip-refusal is sourced ONLY from recorded provider geometry — the +/// removed authored instrument floor (the hand-authored "vetted" table) no +/// longer participates in the refusal path. The cycle-0074 headline is the floor's +/// REMOVAL, not a rename; the observable signature is that the refusal message +/// never points the user at a hand-authored table to extend. A regression that +/// re-introduced an authored fallback (or restored the old "add it to the +/// instrument table" wording) would re-leak this vocabulary. Asserts the OLD +/// authored-floor phrasing is ABSENT — the negative complement to the positive +/// "no recorded geometry" assertion above. Archive-independent: "NONEXISTENT" has +/// no sidecar on any host, so the refusal fires without local data. +#[test] +fn run_real_refusal_names_no_authored_floor() { + 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("instrument table"), + "refusal must not point at a removed authored table, got: {stderr}" + ); + assert!( + !stderr.contains("vetted"), + "refusal must not use the removed vetted-floor vocabulary, got: {stderr}" + ); + assert!( + !stderr.contains("instrument spec"), + "refusal must not reference the removed authored instrument floor, 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 @@ -155,7 +187,7 @@ fn run_real_unspecced_symbol_refuses_with_exit_2() { /// 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() { +fn run_real_no_geometry_symbol_emits_no_stdout_report() { let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args(["run", "--real", "NONEXISTENT"]) .output() @@ -168,25 +200,25 @@ fn run_real_unspecced_symbol_emits_no_stdout_report() { ); } -/// 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). +/// Property: the pip-honesty refusal keys on the SYMBOL having no recorded geometry, +/// not on `--real` being taken at all. A symbol *with* a recorded sidecar (`EURUSD`) +/// gets PAST the geometry 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 recorded geometry" +/// 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() { +fn run_real_sidecar_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}" + !stderr.contains("no recorded geometry"), + "a symbol with a recorded sidecar 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). @@ -200,24 +232,24 @@ fn run_real_vetted_symbol_never_hits_the_pip_refusal() { ), 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}" + "exit 2 for a symbol with a recorded sidecar must be the no-data path, got: {stderr}" ), - other => panic!("unexpected exit for a vetted real run: {other:?}; stderr: {stderr}"), + other => panic!("unexpected exit for a real run with a recorded sidecar: {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 +/// Property: a recorded-sidecar 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 +/// vs EURUSD=0.0001 vs no-geometry→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 +fn run_real_sidecar_index_pip_reaches_the_emitted_manifest() { + // The 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"; @@ -226,13 +258,13 @@ fn run_real_vetted_index_pip_reaches_the_emitted_manifest() { .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. + // Skip on a data-less machine: a recorded-sidecar 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}" + "exit 2 for GER40 must be the no-data path, got: {stderr}" ); eprintln!("skip: no local GER40 data"); return; @@ -248,6 +280,43 @@ fn run_real_vetted_index_pip_reaches_the_emitted_manifest() { ); } +/// Property: the authored instrument table no longer gates real runs — a symbol +/// absent from the (removed) vetted floor but carrying a recorded geometry sidecar +/// resolves its pip from the sidecar and runs. USDJPY (never vetted; JPY pip 0.01) +/// renders the JPY pip in the manifest, proving the pip is sourced from the provider +/// geometry, not a hand-authored table entry. Gated: skips cleanly when USDJPY has +/// no local geometry/data (no sidecar → "no recorded geometry"; sidecar but no bars +/// → "no local data"). +#[test] +fn run_real_nonvetted_symbol_resolves_pip_from_sidecar() { + // FX trades continuously; the GER40 Sept-2024 window also has USDJPY bars. + const FROM_MS: &str = "1725148800000"; + const TO_MS: &str = "1727740799999"; + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args(["run", "--real", "USDJPY", "--from", FROM_MS, "--to", TO_MS]) + .output() + .expect("spawn aura"); + + // Skip on a host without USDJPY geometry/data — either refusal is a clean skip, + // sourced from recorded geometry, never an authored-table miss. + if out.status.code() == Some(2) { + let stderr = String::from_utf8_lossy(&out.stderr); + if stderr.contains("no recorded geometry") || stderr.contains("no local data") { + eprintln!("skip: no local USDJPY geometry/data"); + return; + } + } + + assert_eq!(out.status.code(), Some(0), "USDJPY should resolve and run; exit: {:?}, stderr: {}", + out.status, String::from_utf8_lossy(&out.stderr)); + let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout"); + assert!( + stdout.contains("pip_size=0.01"), + "USDJPY must render the JPY sidecar pip (0.01) in the manifest, got: {}", + stdout.trim_end() + ); +} + #[test] fn run_trace_persists_taps_and_plain_run_writes_no_traces() { let cwd = temp_cwd("run-trace"); @@ -372,13 +441,13 @@ fn run_real_with_trace_persists_taps_and_keeps_stdout_unchanged() { .output() .expect("spawn aura run --real --trace"); - // Skip on a data-less machine: vetted GER40 with no local data takes the + // Skip on a data-less machine: GER40 with no local data takes the // distinct no-data path (exit 2), never the pip-refusal, and writes no traces. 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 --trace must be the no-data path, got: {stderr}" + "exit 2 for GER40 --trace must be the no-data path, got: {stderr}" ); eprintln!("skip: no local GER40 data"); let _ = std::fs::remove_dir_all(&cwd); @@ -1098,10 +1167,10 @@ fn momentum_longonly_true_clamps_exposure_nonnegative_false_keeps_negatives() { // These drive `aura sweep|walkforward --real ` end to end over the local // data-server archive. The gated tests need the Pepperstone M1 archive present; // they skip (print + pass) on a data-less machine, the project's -// skip-on-no-data convention. The pip-refusal test is NOT gated: the un-specced -// symbol is rejected before any data access. +// skip-on-no-data convention. The pip-refusal test is NOT gated: a symbol with no +// recorded geometry is rejected before any data access. -// EURUSD 2024-06 (UTC inclusive ms): a bounded vetted real window. +// EURUSD 2024-06 (UTC inclusive ms): a bounded recorded-sidecar real window. const EURUSD_JUN2024_FROM_MS: i64 = 1_717_200_000_000; const EURUSD_JUN2024_TO_MS: i64 = 1_719_791_999_999; @@ -1181,18 +1250,18 @@ fn walkforward_real_persists_one_oos_member_per_window() { } /// Property: the per-instrument pip refusal fires for `aura sweep --real -/// ` exactly as it does for `aura run --real` — an un-vetted symbol -/// (`AAPL.US`, absent from the instrument table) is rejected with exit 2 and the -/// "no vetted pip" message BEFORE any data access. NOT gated: the refusal -/// precedes the archive entirely, so it is CI-safe and runs on every machine. +/// ` exactly as it does for `aura run --real` — a symbol with no +/// recorded geometry (`NONEXISTENT`, no sidecar on any host) is rejected with exit 2 +/// and the "no recorded geometry" message BEFORE any bar-data access. NOT gated: the +/// refusal precedes the archive entirely, so it is CI-safe and runs on every machine. #[test] -fn sweep_real_unspecced_symbol_refuses_with_exit_2() { +fn sweep_real_no_geometry_symbol_refuses_with_exit_2() { // not gated: the pip refusal happens before any data access. let dir = temp_cwd("sweep_real_refuse"); let out = std::process::Command::new(BIN) - .args(["sweep", "--real", "AAPL.US"]).current_dir(&dir).output().unwrap(); + .args(["sweep", "--real", "NONEXISTENT"]).current_dir(&dir).output().unwrap(); assert_eq!(out.status.code(), Some(2)); - assert!(String::from_utf8_lossy(&out.stderr).contains("no vetted pip")); + assert!(String::from_utf8_lossy(&out.stderr).contains("no recorded geometry")); let _ = std::fs::remove_dir_all(&dir); } diff --git a/crates/aura-engine/src/report.rs b/crates/aura-engine/src/report.rs index ab193e5..8421119 100644 --- a/crates/aura-engine/src/report.rs +++ b/crates/aura-engine/src/report.rs @@ -224,9 +224,9 @@ pub fn summarize_r(record: &[(Timestamp, Vec)], round_trip_cost: f64) -> /// executed book (`deal = target - book - in_flight`; `in_flight = 0` for the /// instant-fill backtest). Pure (C1); no look-ahead — each event's `event_ts` is /// its own cycle (C2). `instrument_id` is supplied by the caller (the engine never -/// imports `InstrumentSpec`). A reversal emits `Close` then the opposite open at -/// one `event_ts` (close first); a position still open on the last row emits its -/// open with no synthetic `Close` (the table records actual executed events — +/// imports an instrument spec from `aura-ingest`). A reversal emits `Close` then the +/// opposite open at one `event_ts` (close first); a position still open on the last +/// row emits its open with no synthetic `Close` (the table records actual executed events — /// unlike `summarize_r`, which force-closes for the R metric). pub fn derive_position_events( record: &[(Timestamp, Vec)], diff --git a/crates/aura-ingest/examples/ger40_breakout_compare.rs b/crates/aura-ingest/examples/ger40_breakout_compare.rs index dd36143..311e312 100644 --- a/crates/aura-ingest/examples/ger40_breakout_compare.rs +++ b/crates/aura-ingest/examples/ger40_breakout_compare.rs @@ -5,7 +5,7 @@ //! 1. **Instrument** — GER40 vs FRA40 (both `pip_size = 1.0`, index points), the //! SAME blueprint bootstrapped at the same `{entry=3, exit=5}` point over each //! symbol. The pip honesty is by construction (both quote in index points; the -//! pip is sourced per cell from the central `instrument_spec` channel — #22 +//! pip is sourced per cell from the recorded geometry sidecar — #22 //! shipped). //! 2. **Bar period** — a 15m blueprint vs a 30m blueprint. The bar period is a //! STRUCTURAL axis (C34): `ger40_breakout_blueprint` is instantiated TWICE, @@ -40,14 +40,14 @@ const EXIT_BAR: i64 = 5; // GER40 and FRA40 both quote in index points, so pip_size = 1.0 for both — the // only reason a cross-instrument pip comparison is honest here. The pip is sourced -// per cell from the central vetted `instrument_spec` channel (#22 shipped), not a +// per cell from the recorded geometry sidecar (#22 shipped), not a // literal carried here. (symbol, human note) const INSTRUMENTS: &[(&str, &str)] = &[("GER40", "index point"), ("FRA40", "index point")]; /// Bootstrap the blueprint (at `bar_period`) at `{entry=3, exit=5}`, run it over /// `[from, to]` (epoch-ns) of real `symbol` OHLC, and summarize — or `None` if the /// symbol has no file overlapping the window. The `pip_size` is sourced per cell -/// from the central vetted `instrument_spec` channel (always 1.0 here; both +/// from the recorded geometry sidecar (always 1.0 here; both /// indices) and rides on the blueprint's baked SimBroker. Returns the report plus /// the entry count (0→1 transitions of held). fn run_cell( diff --git a/crates/aura-ingest/examples/shared/breakout_real.rs b/crates/aura-ingest/examples/shared/breakout_real.rs index 62b4eb2..5cb9862 100644 --- a/crates/aura-ingest/examples/shared/breakout_real.rs +++ b/crates/aura-ingest/examples/shared/breakout_real.rs @@ -68,14 +68,12 @@ pub const REC_BREAKOUT: usize = 12; // tap D — Gt breakout flag (bool) /// The instrument. GER40 is an index, so pips are index points (`pip_size = 1.0`). pub const SYMBOL: &str = "GER40"; -/// The SimBroker pip divisor for `symbol`, sourced from the central vetted -/// instrument-metadata channel ([`aura_ingest::instrument_spec`], C7/C15) instead of a -/// baked literal — so these examples consume the same pip table the CLI `--real` path -/// does, and a cross-instrument comparison is honest by construction (#98). Panics on -/// an un-vetted symbol (the examples only ever run known instruments). +/// The example's pip divisor, sourced from the recorded geometry sidecar — the same +/// provider truth the CLI `--real` path uses, now provider-sourced. Panics on a +/// symbol with no sidecar (the examples only run known instruments with local data). pub fn pip_size_of(symbol: &str) -> f64 { - aura_ingest::instrument_spec(symbol) - .unwrap_or_else(|| panic!("no vetted instrument spec for {symbol}")) + aura_ingest::instrument_geometry(&aura_ingest::default_data_server(), symbol) + .unwrap_or_else(|| panic!("no recorded geometry sidecar for {symbol}")) .pip_size } diff --git a/crates/aura-ingest/src/lib.rs b/crates/aura-ingest/src/lib.rs index 6e2c0d0..48cac48 100644 --- a/crates/aura-ingest/src/lib.rs +++ b/crates/aura-ingest/src/lib.rs @@ -349,8 +349,9 @@ pub fn default_data_server() -> Arc { /// /// Surfaces the tier-2 *source*; the tier-composing, authored-override-wins /// *resolver* is #124. The geometry is an **untrusted input** the authored override -/// supersedes — never a silent runtime fallback for an unvetted symbol -/// (refuse-don't-guess stands: [`instrument_spec`] still returns `None`). It is +/// supersedes — never a silent runtime fallback for an unvetted symbol. Returns +/// `None` for a symbol with no recorded sidecar — the caller refuses rather than +/// guess a pip. It is /// non-scalar reference metadata held beside the hot path (C15); it is **not** a /// [`Source`](aura_engine::Source) and never enters the engine's /// `(Timestamp, Scalar)` seam. @@ -358,83 +359,6 @@ pub fn instrument_geometry(server: &Arc, symbol: &str) -> Option 0). The sim-optimal broker's divisor. - pub pip_size: f64, - /// Units of the base instrument per 1.0 lot (FX standard lot = 100_000; - /// an index CFD = 1). - pub contract_size: f64, - /// Value of one pip per 1.0 lot, in the quote currency. Equals - /// `contract_size * pip_size` for every vetted instrument here; carried - /// explicitly because a real broker may not make it exactly so. - pub pip_value_per_lot: f64, - /// Smallest tradable volume and the volume granularity, both in lots. - pub min_lot: f64, - pub lot_step: f64, - /// Quote currency (ISO label only this cycle — no FX conversion yet). - pub quote_currency: &'static str, - /// Smallest representable price increment (provider geometry; deploy-grade - /// price rounding). 1e-5 for a 5-decimal FX major, 0.1 for the indices. - pub tick_size: f64, - /// Decimal places in a quoted price (provider geometry; deploy-grade price - /// rounding). 5 for FX majors, 1 for the indices. - pub digits: u32, -} - -/// The vetted symbols [`instrument_spec`] resolves — the single named truth of the -/// authored floor's membership (#113), so consumers iterate one list rather than -/// re-spelling the set. Extending the table (#140) extends this constant and only -/// this constant; every test that sweeps the floor reads it, so the copies cannot -/// drift. Order matches the `instrument_id` assignment in [`instrument_spec`]. -pub const VETTED_SYMBOLS: [&str; 5] = ["GER40", "FRA40", "EURUSD", "GBPUSD", "USDCAD"]; - -/// Look up per-instrument reference metadata by `symbol` at the ingestion edge. -/// Rust-authored vetted table (NOT `Aura.toml`); `None` for an instrument with no -/// vetted spec — the caller must refuse rather than guess a pip. -/// -/// Seeded with the instruments the engine's own paths exercise. Indices quote in -/// points (pip = 1.0); 5-decimal FX majors = 0.0001. NB: JPY pairs are 0.01, NOT -/// 0.0001 — only vetted values are seeded. -pub fn instrument_spec(symbol: &str) -> Option { - let s = match symbol { - "GER40" => InstrumentSpec { - instrument_id: 1, pip_size: 1.0, contract_size: 1.0, - pip_value_per_lot: 1.0, min_lot: 0.1, lot_step: 0.01, quote_currency: "EUR", - tick_size: 0.1, digits: 1, - }, - "FRA40" => InstrumentSpec { - instrument_id: 2, pip_size: 1.0, contract_size: 1.0, - pip_value_per_lot: 1.0, min_lot: 0.1, lot_step: 0.01, quote_currency: "EUR", - tick_size: 0.1, digits: 1, - }, - "EURUSD" => InstrumentSpec { - instrument_id: 3, pip_size: 0.0001, contract_size: 100_000.0, - pip_value_per_lot: 10.0, min_lot: 0.01, lot_step: 0.01, quote_currency: "USD", - tick_size: 1e-5, digits: 5, - }, - "GBPUSD" => InstrumentSpec { - instrument_id: 4, pip_size: 0.0001, contract_size: 100_000.0, - pip_value_per_lot: 10.0, min_lot: 0.01, lot_step: 0.01, quote_currency: "USD", - tick_size: 1e-5, digits: 5, - }, - "USDCAD" => InstrumentSpec { - instrument_id: 5, pip_size: 0.0001, contract_size: 100_000.0, - pip_value_per_lot: 10.0, min_lot: 0.01, lot_step: 0.01, quote_currency: "CAD", - tick_size: 1e-5, digits: 5, - }, - _ => return None, - }; - Some(s) -} - #[cfg(test)] mod tests { use super::*; @@ -450,112 +374,6 @@ mod tests { assert!(instrument_geometry(&server, "NOT_A_REAL_SYMBOL").is_none()); } - #[test] - fn instrument_spec_lookup_by_symbol() { - // index CFDs: quote EUR, 1 contract = €1/point, pip = 1.0 point - assert_eq!( - instrument_spec("GER40"), - Some(InstrumentSpec { - instrument_id: 1, - pip_size: 1.0, - contract_size: 1.0, - pip_value_per_lot: 1.0, - min_lot: 0.1, - lot_step: 0.01, - quote_currency: "EUR", - tick_size: 0.1, - digits: 1, - }) - ); - assert_eq!( - instrument_spec("FRA40"), - Some(InstrumentSpec { - instrument_id: 2, - pip_size: 1.0, - contract_size: 1.0, - pip_value_per_lot: 1.0, - min_lot: 0.1, - lot_step: 0.01, - quote_currency: "EUR", - tick_size: 0.1, - digits: 1, - }) - ); - // FX majors: standard lot = 100_000 units, pip = 0.0001, pip value = 10 in quote ccy - assert_eq!( - instrument_spec("EURUSD"), - Some(InstrumentSpec { - instrument_id: 3, - pip_size: 0.0001, - contract_size: 100_000.0, - pip_value_per_lot: 10.0, - min_lot: 0.01, - lot_step: 0.01, - quote_currency: "USD", - tick_size: 1e-5, - digits: 5, - }) - ); - assert_eq!( - instrument_spec("GBPUSD"), - Some(InstrumentSpec { - instrument_id: 4, - pip_size: 0.0001, - contract_size: 100_000.0, - pip_value_per_lot: 10.0, - min_lot: 0.01, - lot_step: 0.01, - quote_currency: "USD", - tick_size: 1e-5, - digits: 5, - }) - ); - assert_eq!( - instrument_spec("USDCAD"), - Some(InstrumentSpec { - instrument_id: 5, - pip_size: 0.0001, - contract_size: 100_000.0, - pip_value_per_lot: 10.0, - min_lot: 0.01, - lot_step: 0.01, - quote_currency: "CAD", - tick_size: 1e-5, - digits: 5, - }) - ); - // un-specced → None (the honesty lever) - assert_eq!(instrument_spec("NONEXISTENT"), None); - } - - #[test] - fn instrument_spec_pip_value_matches_contract_times_pip() { - // pip_value_per_lot == contract_size * pip_size for every vetted instrument - // (cross-field invariant, not the literals' real-world exactness). - for sym in VETTED_SYMBOLS { - let s = instrument_spec(sym).expect("vetted symbol"); - assert!( - (s.pip_value_per_lot - s.contract_size * s.pip_size).abs() < 1e-9, - "{sym}: pip_value_per_lot {} != contract_size {} * pip_size {}", - s.pip_value_per_lot, - s.contract_size, - s.pip_size - ); - } - } - - #[test] - fn instrument_spec_ids_are_distinct() { - let ids: Vec = VETTED_SYMBOLS - .iter() - .map(|s| instrument_spec(s).expect("vetted symbol").instrument_id) - .collect(); - let mut sorted = ids.clone(); - sorted.sort_unstable(); - sorted.dedup(); - assert_eq!(sorted.len(), ids.len(), "instrument_ids must be unique: {ids:?}"); - } - #[test] fn unix_ms_to_epoch_ns_scales_ms_to_ns() { // data-server's own epoch fixture: 2017-03-01 00:00 UTC. diff --git a/crates/aura-ingest/tests/ger40_breakout_blueprint.rs b/crates/aura-ingest/tests/ger40_breakout_blueprint.rs index 43b2cac..18873a8 100644 --- a/crates/aura-ingest/tests/ger40_breakout_blueprint.rs +++ b/crates/aura-ingest/tests/ger40_breakout_blueprint.rs @@ -37,7 +37,9 @@ const MONTH: u32 = 9; /// be swept into a desync. No data needed — runs everywhere. #[test] fn param_space_is_exactly_entry_and_exit_bar_targets() { - let (bp, _taps) = ger40_breakout_blueprint(pip_size_of(SYMBOL), 15, 9, 0, Berlin); + // pure-structural: the pip is a throwaway sizing arg, never asserted here, so a + // literal keeps this archive-independent (the topology assertion is the point). + let (bp, _taps) = ger40_breakout_blueprint(1.0, 15, 9, 0, Berlin); let space = bp.param_space(); let names: Vec<&str> = space.iter().map(|p| p.name.as_str()).collect(); @@ -64,8 +66,8 @@ fn param_space_is_exactly_entry_and_exit_bar_targets() { /// `composite_matches_flatgraph_bit_identical`.) #[test] fn blueprint_param_space_is_construction_deterministic() { - let a = ger40_breakout_blueprint(pip_size_of(SYMBOL), 15, 9, 0, Berlin).0.param_space(); - let b = ger40_breakout_blueprint(pip_size_of(SYMBOL), 15, 9, 0, Berlin).0.param_space(); + let a = ger40_breakout_blueprint(1.0, 15, 9, 0, Berlin).0.param_space(); + let b = ger40_breakout_blueprint(1.0, 15, 9, 0, Berlin).0.param_space(); assert_eq!(a, b, "blueprint param_space() is a deterministic function of its args"); } diff --git a/crates/aura-ingest/tests/instrument_geometry.rs b/crates/aura-ingest/tests/instrument_geometry.rs new file mode 100644 index 0000000..f86dd3b --- /dev/null +++ b/crates/aura-ingest/tests/instrument_geometry.rs @@ -0,0 +1,12 @@ +//! Guards the surviving provider-geometry seam: `aura_ingest::instrument_geometry` +//! and the `InstrumentGeometry` re-export must stay nameable from outside the crate +//! (a downstream broker consumes them). Dropping the re-export would break this. +use aura_ingest::{default_data_server, instrument_geometry, InstrumentGeometry}; + +#[test] +fn provider_geometry_type_is_nameable_through_the_ingest_seam() { + // Name the re-exported type explicitly (not just `_`), so removing the + // `pub use data_server::meta::InstrumentGeometry` re-export fails to compile here. + let server = default_data_server(); + let _maybe: Option = instrument_geometry(&server, "EURUSD"); +} diff --git a/crates/aura-ingest/tests/instrument_geometry_crosscheck.rs b/crates/aura-ingest/tests/instrument_geometry_crosscheck.rs deleted file mode 100644 index 518c442..0000000 --- a/crates/aura-ingest/tests/instrument_geometry_crosscheck.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Cross-check the hand-authored vetted InstrumentSpec floor (#113) against the -//! provider-recorded geometry sidecars (#143 — tier 2 of #124's resolution -//! hierarchy). Skips with a note where the local Pepperstone archive is absent, -//! so it is hermetic elsewhere and exercises broker truth where the files exist. - -use aura_ingest::{ - default_data_server, instrument_geometry, instrument_spec, InstrumentGeometry, - DEFAULT_DATA_PATH, VETTED_SYMBOLS, -}; - -/// Property: the provider geometry type is **nameable through `aura-ingest` -/// alone** — the load-bearing seam contract of #143's re-export. A consumer that -/// wants to store or type-annotate the provider geometry (not just call the -/// accessor and immediately destructure it) writes `aura_ingest::InstrumentGeometry` -/// and never reaches into `data_server`. This pins that re-export as an -/// observable, consumer-facing fact: the accessor's `Option<_>` binds to the -/// crate-re-exported type, and a real sidecar yields a value carrying sane -/// provider fields. Dropping the `pub use data_server::meta::InstrumentGeometry` -/// would break this compile while leaving the bare-accessor tests green (they -/// never name the type), so it guards exactly the seam a refactor could silently -/// sever. Skip-if-absent, same discipline as the cross-check below. -#[test] -fn provider_geometry_type_is_nameable_through_the_ingest_seam() { - let server = default_data_server(); - let maybe: Option = instrument_geometry(&server, "EURUSD"); - let Some(geo) = maybe else { - eprintln!("skip: no local geometry sidecars at {DEFAULT_DATA_PATH} (EURUSD.meta.json absent)"); - return; - }; - // Bind into the re-exported type by name (not just `geo.field` off an inferred - // type) so the test fails to compile if the re-export is removed. - let g: InstrumentGeometry = geo; - assert!(g.pip_size > 0.0, "EURUSD provider pip_size must be positive"); - assert!(g.tick_size > 0.0, "EURUSD provider tick_size must be positive"); - assert!(g.digits > 0, "EURUSD provider digits must be positive"); - assert!(g.lot_size > 0.0, "EURUSD provider lot_size must be positive"); - assert!(!g.quote.is_empty(), "EURUSD provider quote currency must be labelled"); -} - -#[test] -fn authored_floor_agrees_with_provider_geometry() { - let server = default_data_server(); - // Gate on a sidecar's presence (symbol_meta reads the file directly, not the - // bar index) — the same skip-if-absent discipline as the other real-data tests. - if instrument_geometry(&server, "EURUSD").is_none() { - eprintln!("skip: no local geometry sidecars at {DEFAULT_DATA_PATH} (EURUSD.meta.json absent)"); - return; - } - for sym in VETTED_SYMBOLS { - let spec = instrument_spec(sym).expect("vetted symbol"); - let geo = instrument_geometry(&server, sym) - .unwrap_or_else(|| panic!("vetted symbol {sym} must carry a geometry sidecar")); - - assert!( - (spec.pip_size - geo.pip_size).abs() < 1e-12, - "{sym}: authored pip_size {} != provider {}", spec.pip_size, geo.pip_size - ); - assert!( - (spec.contract_size - geo.lot_size).abs() < 1e-6, - "{sym}: authored contract_size {} != provider lot_size {}", spec.contract_size, geo.lot_size - ); - // the C10 pip-value invariant, now against broker truth - let provider_pip_value = geo.lot_size * geo.pip_size; - assert!( - (spec.pip_value_per_lot - provider_pip_value).abs() < 1e-9, - "{sym}: authored pip_value_per_lot {} != provider lot_size*pip_size {}", - spec.pip_value_per_lot, provider_pip_value - ); - assert!( - (spec.tick_size - geo.tick_size).abs() < 1e-12, - "{sym}: authored tick_size {} != provider {}", spec.tick_size, geo.tick_size - ); - assert_eq!( - spec.digits, geo.digits, - "{sym}: authored digits {} != provider {}", spec.digits, geo.digits - ); - assert_eq!( - spec.quote_currency, geo.quote.as_str(), - "{sym}: authored quote_currency {} != provider {}", spec.quote_currency, geo.quote - ); - } -} diff --git a/crates/aura-ingest/tests/instrument_spec_deploy_metadata.rs b/crates/aura-ingest/tests/instrument_spec_deploy_metadata.rs deleted file mode 100644 index 2d00310..0000000 --- a/crates/aura-ingest/tests/instrument_spec_deploy_metadata.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! End-to-end verification of the public instrument-spec resolution seam -//! (#113): a downstream broker resolves deploy-grade metadata by symbol through -//! `aura_ingest::instrument_spec` — the permanent authored floor of the -//! milestone's override hierarchy (#124). These tests pin the *consumer-visible* -//! contract (the public return shape + the refuse-don't-guess arm), distinct -//! from the crate-internal literal-table unit tests. - -use aura_ingest::{instrument_spec, InstrumentSpec, VETTED_SYMBOLS}; - -/// Property: every vetted symbol resolves through the *public* lookup to a fully -/// populated `InstrumentSpec` whose deploy fields are all present and sane -/// (positive sizes/ids, positive tick geometry, a non-empty quote currency). -/// This is the floor a broker -/// reads before sizing a lot; a regression that left any field at a placeholder -/// (0.0 / "") or dropped a symbol from the table would size positions wrong with -/// no error. Asserts only the consumer-visible guarantees, not the exact vetted -/// literals (those are the crate's own unit test's remit). -#[test] -fn every_vetted_symbol_resolves_to_a_complete_spec() { - for sym in VETTED_SYMBOLS { - let s: InstrumentSpec = instrument_spec(sym).unwrap_or_else(|| { - panic!("vetted symbol {sym} must resolve through the public lookup") - }); - assert!(s.instrument_id > 0, "{sym}: instrument_id must be a real (>0) id"); - assert!(s.pip_size > 0.0, "{sym}: pip_size must be positive"); - assert!(s.contract_size > 0.0, "{sym}: contract_size must be positive"); - assert!(s.pip_value_per_lot > 0.0, "{sym}: pip_value_per_lot must be positive"); - assert!(s.min_lot > 0.0, "{sym}: min_lot must be positive"); - assert!(s.lot_step > 0.0, "{sym}: lot_step must be positive"); - assert!(!s.quote_currency.is_empty(), "{sym}: quote_currency must be labelled"); - assert!(s.tick_size > 0.0, "{sym}: tick_size must be positive"); - assert!(s.digits > 0, "{sym}: digits must be positive"); - } -} - -/// Property: the cross-field deploy invariant `tick_size == 10^(-digits)` holds -/// for every vetted instrument as a downstream broker observes it through the -/// public lookup. `tick_size` and `digits` are two views of the same provider -/// price geometry; a broker may round a price by either. This pins that the two -/// agree, so a consistent typo'd *pair* in one table entry (which the positivity -/// asserts and an equally-typo'd literal unit test would both wave through) is -/// caught — exactly the silent-wrong-rounding regression this file guards. -#[test] -fn tick_size_is_consistent_with_digits_at_the_public_boundary() { - for sym in VETTED_SYMBOLS { - let s = instrument_spec(sym).expect("vetted symbol"); - let derived = 10f64.powi(-(s.digits as i32)); - assert!( - (s.tick_size - derived).abs() < 1e-12, - "{sym}: tick_size {} must equal 10^(-digits) = 10^(-{}) = {}", - s.tick_size, - s.digits, - derived, - ); - } -} - -/// Property: the cross-field deploy invariant `pip_value_per_lot == -/// contract_size * pip_size` holds for every vetted instrument as a downstream -/// broker observes it through the public lookup. A broker computing pip P&L may -/// use either `pip_value_per_lot` directly or derive it from contract_size × -/// pip_size; this pins that the two paths agree, so the choice is free. A future -/// table edit that broke the identity (e.g. a typo'd pip_value) would silently -/// double or halve every realistic-broker P&L. -#[test] -fn pip_value_is_consistent_with_contract_and_pip_at_the_public_boundary() { - for sym in VETTED_SYMBOLS { - let s = instrument_spec(sym).expect("vetted symbol"); - let derived = s.contract_size * s.pip_size; - assert!( - (s.pip_value_per_lot - derived).abs() < 1e-9, - "{sym}: pip_value_per_lot {} must equal contract_size {} * pip_size {} = {}", - s.pip_value_per_lot, - s.contract_size, - s.pip_size, - derived, - ); - } -} - -/// Property: an un-vetted symbol resolves to `None` — the refuse-don't-guess -/// honesty lever (#113). A broker that asked for a symbol with no authored floor -/// must get nothing back, never a fabricated/defaulted spec, so deploy fails -/// loudly rather than sizing on a guessed pip value. A regression that returned -/// a `Some(default)` for unknown symbols is exactly the silent-guess failure -/// this guards. -#[test] -fn unvetted_symbol_refuses_with_none() { - assert_eq!(instrument_spec("NOT_A_REAL_SYMBOL"), None); - assert_eq!(instrument_spec(""), None); - // case sensitivity is part of the refuse arm: the table is exact-match only. - assert_eq!(instrument_spec("ger40"), None); -} diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index cd9f25c..bdbc244 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -602,11 +602,14 @@ table (buy/sell/close, `position_id`, partial closes) from the exposure history, and the realistic broker nodes that consume it — is deliberately **not** built this cycle; it remains the decoupled, derived, deferred layer described above. **Realization (per-instrument pip channel, 2026-06, #22).** `SimBroker`'s -`pip_size` is now sourced **per instrument**, not from one global literal. A typed -`InstrumentSpec { pip_size }` + `instrument_spec(symbol) -> Option` -lookup — a Rust-authored vetted table in `aura-ingest`, at the ingestion/source -edge where the symbol still exists (never `Aura.toml`, never `Ctx`) — supplies the -divisor; the engine stays domain-free (no instrument identity reaches the hot path). +`pip_size` is now sourced **per instrument**, not from one global literal. The +divisor is resolved from the recorded geometry sidecar (`instrument_geometry`, over +data-server's `symbol_meta`), at the ingestion/source edge where the symbol still +exists (never `Aura.toml`, never `Ctx`); the engine stays domain-free (no instrument +identity reaches the hot path). (The original cycle-0022 form was a Rust-authored +vetted floor `InstrumentSpec { pip_size }` + `instrument_spec(symbol)`; cycle 0074 +removed that floor — see the C15 note below — once the sidecar geometry made it +redundant for the real path. Refuse-don't-guess on absent geometry.) **Realization (position-event schema, cycle 0063, #114).** The position-management half's **schema** now landed (its derivation and the brokers remain deferred): a closed `PositionAction { Buy, Sell, Close }` enum + the `PositionEvent` row @@ -706,7 +709,8 @@ stop-then-same-cycle reopen) emitting **Close then the opposite open at one `eve table, not a per-`eval` output). The close sizes the **actual book** (the closed position's stored volume), never an exposure delta — the post-reframe replacement for the rolled-back 0064 exposure-integral derive (#117). `instrument_id` is a caller-supplied -scalar (`aura-engine` depends only on `aura-core`, so it never imports `InstrumentSpec`). +scalar (`aura-engine` depends only on `aura-core`, so it never imports an instrument +spec from `aura-ingest`). A position open at window end emits its open with **no synthetic `Close`** (the table records actual executed events; `summarize_r`'s force-close is for the R metric only). The `r_col` ⟷ PM-record lockstep is now guard-pinned for `direction` too. **Still #116:** @@ -812,26 +816,25 @@ session open" is then a plain node checking `bars_since_open == 3`. stream model. **Why.** Keeps the line consistent — everything a signal needs arrives as a stream; reference data feeds source/session nodes from beside the hot path. -**Realization (instrument specs, 2026-06, #22).** The "instrument specs are -metadata" half of this contract is first realized by `aura-ingest`'s -`InstrumentSpec` (initially a single `pip_size`) + `instrument_spec(symbol)` — -non-scalar reference data held beside the hot path, keyed by symbol, feeding the -sim-optimal broker's pip divisor (C10). Extended in cycle 0063 (#113) to a six-field -deploy-grade floor (`instrument_id`, `contract_size`, `pip_value_per_lot`, `min_lot`, -`lot_step`, `quote_currency`) for the realistic broker's currency conversion — the -permanent authored floor of the override hierarchy (#124), the refuse-don't-guess -arm preserved. NB: `quote_currency` is `&'static str` (keeps the struct `Copy`); how -the #124 resolver's runtime-sourced tier carries currency (a distinct resolved type, -or widening the field) is that cycle's decision — see #124. Cycle 0073 (#143) -appended `tick_size` + `digits` (provider price-rounding geometry; still additive and -`Copy`) and surfaced the **recorded-metadata tier-2 source** at the ingestion edge: +**Realization (instrument specs, 2026-06, #22 → #124).** The "instrument specs are +metadata" half of this contract is realized by the **recorded geometry sidecar**: `instrument_geometry(server, symbol)`, over data-server's `symbol_meta`, returns neutral broker-agnostic `InstrumentGeometry` (the raw provider JSON never enters this -repo), and a build/test-time cross-check pins the authored floor against that provider -truth for the vetted set. The geometry is an untrusted input the authored override -supersedes — non-scalar metadata beside the hot path, never a `Source`. The -tier-*composing* resolver, and the `quote_currency` runtime-type transition, remain -#124's. +repo) — non-scalar reference data held beside the hot path, keyed by symbol, feeding +the sim-optimal broker's pip divisor (C10). The real-path pip is sourced from +`InstrumentGeometry.pip_size`; refuse-don't-guess on absent geometry. **History:** +cycles 0022/0063 first carried a Rust-authored vetted floor (`InstrumentSpec` — a +single `pip_size`, later a six-field deploy-grade row `instrument_id`, +`contract_size`, `pip_value_per_lot`, `min_lot`, `lot_step`, `quote_currency`, plus +`tick_size`/`digits`), cross-checked against the sidecar geometry. **Cycle 0074 +removed that floor**: with the sidecar geometry supplying the real-path pip, the +authored table was redundant, so `InstrumentSpec` / `instrument_spec` / the vetted +list were deleted. The speculative deploy-grade fields and the override/floor tiers +of the #124 hierarchy (tier 1 authored override, tier 3 authored floor) are +**deferred** until a consumer — the Stage-2 realistic broker — needs them, read from +the sidecar geometry then. The `quote_currency` runtime-type transition is likewise +deferred to that consumer; #124 collapses to **recorded sidecar geometry → refuse** +in the meantime. ### C16 — Engine / project separation; three-tier node reuse **Guarantee.** aura is the reusable **engine**; each research project is a @@ -1233,8 +1236,9 @@ families-comparison amendment below). `M1FieldSource` seam — C6 record-then-replay: a pre-recorded archive, never a live call mid-sim) instead of the built-in synthetic stream, per family member. A CLI-side `DataSource` provider (synthetic | real) supplies each member's source, -pip (`instrument_spec` — C10 refuse-don't-guess; an un-vetted symbol exits 2 before -any data access), window, and — for walk-forward — its `WindowRoller` sizes +pip (resolved from the recorded geometry sidecar (`instrument_geometry`) — C10 +refuse-don't-guess; a symbol with no recorded geometry exits 2 before any data +access), window, and — for walk-forward — its `WindowRoller` sizes (synthetic: bar-index 24/12/12; real: fixed calendar-time 90d/30d/30d in ns; CLI flags for these are deferred). The engine, ingest, and registry are untouched (C9 — the whole change is in `aura-cli`); the synthetic path is byte-unchanged.