diff --git a/crates/aura-ingest/src/lib.rs b/crates/aura-ingest/src/lib.rs index 02e96f3..6e2c0d0 100644 --- a/crates/aura-ingest/src/lib.rs +++ b/crates/aura-ingest/src/lib.rs @@ -532,7 +532,7 @@ mod tests { 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 ["GER40", "FRA40", "EURUSD", "GBPUSD", "USDCAD"] { + 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, @@ -546,7 +546,7 @@ mod tests { #[test] fn instrument_spec_ids_are_distinct() { - let ids: Vec = ["GER40", "FRA40", "EURUSD", "GBPUSD", "USDCAD"] + let ids: Vec = VETTED_SYMBOLS .iter() .map(|s| instrument_spec(s).expect("vetted symbol").instrument_id) .collect(); diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index 0842da3..cd9f25c 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -822,7 +822,16 @@ deploy-grade floor (`instrument_id`, `contract_size`, `pip_value_per_lot`, `min_ 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. +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: +`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. ### C16 — Engine / project separation; three-tier node reuse **Guarantee.** aura is the reusable **engine**; each research project is a diff --git a/docs/plans/0073-instrument-geometry-crosscheck.md b/docs/plans/0073-instrument-geometry-crosscheck.md deleted file mode 100644 index a8db7c0..0000000 --- a/docs/plans/0073-instrument-geometry-crosscheck.md +++ /dev/null @@ -1,309 +0,0 @@ -# Instrument-geometry cross-check + deploy fields — Implementation Plan - -> **Parent spec:** `docs/specs/0073-instrument-geometry-crosscheck.md` -> -> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run -> this plan. Steps use `- [ ]` checkboxes for tracking. - -**Goal:** Consume data-server's neutral `InstrumentGeometry` at the aura-ingest edge, add the two deferred deploy-grade `InstrumentSpec` fields (`tick_size`, `digits`), and cross-check the hand-authored vetted floor against provider truth. - -**Architecture:** All work is in `crates/aura-ingest`. A thin re-export + accessor surface the tier-2 geometry source (the resolver stays #124); two `Copy` scalar fields are appended to `InstrumentSpec` (additive, C15) with values authored from the verified sidecars; a new real-data integration test cross-checks the authored floor against provider geometry, iterating the vetted set (so #140 is covered for free) and skipping where the archive is absent. - -**Tech Stack:** Rust; `crates/aura-ingest/src/lib.rs`; the `data-server` git dependency (`symbol_meta` → `meta::InstrumentGeometry`, lock already at `694f96f2`, commit `d5e9c27`). - ---- - -**Files this plan creates or modifies:** - -- Modify: `crates/aura-ingest/src/lib.rs` — re-export (`:30`), accessor (near `default_data_server` `:338`), `InstrumentSpec` struct (`:347`, fields `:348-364`), `instrument_spec()` table literals (`:376/:380/:384/:388/:392`), unit-test literals (`:410/:420/:433/:445/:457`), one new hermetic unit test in `mod tests`. -- Modify: `crates/aura-ingest/tests/instrument_spec_deploy_metadata.rs:27-33` — two sanity asserts for the new fields. -- Create: `crates/aura-ingest/tests/instrument_geometry_crosscheck.rs` — the real-data cross-check. - -> **Not a task:** the `Cargo.lock` bump to `data-server 694f96f2` is already committed (`d5e9c27`); nothing to do for it this cycle. - -> **Dependency order (sequential, no skip-ahead):** Task 1 (accessor) → Task 2 (fields) → Task 3 (cross-check uses both). Every intermediate state compiles. - ---- - -### Task 1: Geometry-access seam (re-export + accessor) - -**Files:** -- Modify: `crates/aura-ingest/src/lib.rs` (`:30` re-export, near `:340` accessor, `mod tests` new unit test) - -- [ ] **Step 1: Write the failing test** - -In `crates/aura-ingest/src/lib.rs`, inside `#[cfg(test)] mod tests` (which has `use super::*;`), add: - -```rust - #[test] - fn instrument_geometry_none_for_unknown_symbol() { - // A symbol with no sidecar yields None — the accessor surfaces the reader's - // absent/untrusted contract at the aura-ingest edge. Hermetic: a bogus symbol - // has no .meta.json whether or not the local archive is present, and - // `default_data_server()` constructs safely even when the path is absent - // (see tests/open_ohlc_absent_archive.rs). - let server = default_data_server(); - assert!(instrument_geometry(&server, "NOT_A_REAL_SYMBOL").is_none()); - } -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `cargo test -p aura-ingest instrument_geometry_none_for_unknown_symbol` -Expected: FAIL — compile error `cannot find function 'instrument_geometry' in this scope`. - -- [ ] **Step 3: Write minimal implementation** - -In `crates/aura-ingest/src/lib.rs`, immediately after the existing re-export at `:30` (`pub use data_server::{DataServer, DEFAULT_DATA_PATH};`), add: - -```rust -/// Re-export of data-server's neutral instrument geometry (#143): a consumer reads -/// provider geometry through `aura-ingest` alone, never naming `data_server`. -pub use data_server::meta::InstrumentGeometry; -``` - -And near `default_data_server` (after its closing brace at `:340`), add the accessor: - -```rust -/// Load the provider-recorded neutral geometry for `symbol` — the recorded -/// dataset-metadata tier (tier 2) of #124's resolution hierarchy — or `None` when -/// the dataset carries no usable sidecar (missing / unparseable / unsupported -/// version / a non-finite field; the reader absorbs every provider quirk). -/// -/// 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 -/// 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. -pub fn instrument_geometry(server: &Arc, symbol: &str) -> Option { - server.symbol_meta(symbol) -} -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `cargo test -p aura-ingest instrument_geometry_none_for_unknown_symbol` -Expected: PASS (`1 passed`). - ---- - -### Task 2: Two additive `InstrumentSpec` fields (`tick_size`, `digits`) - -**Files:** -- Modify: `crates/aura-ingest/tests/instrument_spec_deploy_metadata.rs:27-33` -- Modify: `crates/aura-ingest/src/lib.rs` (struct `:348-364`; table literals `:376/:380/:384/:388/:392`; unit-test literals `:410/:420/:433/:445/:457`) - -- [ ] **Step 1: Write the failing test** - -In `crates/aura-ingest/tests/instrument_spec_deploy_metadata.rs`, in `every_vetted_symbol_resolves_to_a_complete_spec`, immediately after the existing `quote_currency` assert (`:33`), add: - -```rust - assert!(s.tick_size > 0.0, "{sym}: tick_size must be positive"); - assert!(s.digits > 0, "{sym}: digits must be positive"); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `cargo test -p aura-ingest every_vetted_symbol_resolves_to_a_complete_spec` -Expected: FAIL — compile error `no field 'tick_size' on type 'InstrumentSpec'`. - -- [ ] **Step 3: Write minimal implementation** - -(a) In `crates/aura-ingest/src/lib.rs`, append two fields to `InstrumentSpec` immediately before its closing brace (`:365`, after `quote_currency`): - -```rust - /// 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, -``` - -(b) Update the 5 `instrument_spec()` table literals (the compiler forces every one). After-form of each: - -```rust - "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, - }, -``` - -(c) Update the 5 unit-test literals in `instrument_spec_lookup_by_symbol` (`mod tests`). Each is a field-per-line `Some(InstrumentSpec { .. })`; insert the two new fields immediately before the closing `})`. After-form of each: - -```rust - // GER40 (:410-418 -> +2 lines) - 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, - }) - // 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, - }) - // 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, - }) - // 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, - }) - // 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, - }) -``` - -- [ ] **Step 4: Run test to verify it passes** - -Run: `cargo test -p aura-ingest every_vetted_symbol_resolves_to_a_complete_spec` -Expected: PASS (`1 passed`). The lib unit tests (`instrument_spec_lookup_by_symbol` etc.) also compile and pass. - ---- - -### Task 3: Provider-geometry cross-check (real-data, skip-if-absent) - -**Files:** -- Create: `crates/aura-ingest/tests/instrument_geometry_crosscheck.rs` - -- [ ] **Step 1: Write the test** - -Create `crates/aura-ingest/tests/instrument_geometry_crosscheck.rs`: - -```rust -//! 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, DEFAULT_DATA_PATH}; - -/// The vetted symbols. Kept in lock-step with the authored table; #140 extends it. -const VETTED: [&str; 5] = ["GER40", "FRA40", "EURUSD", "GBPUSD", "USDCAD"]; - -#[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 { - 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 - ); - } -} -``` - -- [ ] **Step 2: Run the test** - -Run: `cargo test -p aura-ingest authored_floor_agrees_with_provider_geometry` -Expected: PASS (`1 passed`). This is a *verification* test: it is green on the first correct run because the authored fields were taken from the same sidecars; its value is failing on a FUTURE divergence (a table typo, or a #140 symbol whose hand-registered values disagree with the provider). The archive IS present on this host (`/mnt/tickdata/Pepperstone`, 30 sidecars), so the test runs rather than skips — confirm the output is `1 passed`, NOT a skip line. - ---- - -### Final gates (run once, after all tasks) - -- [ ] **Workspace test suite** - -Run: `cargo test --workspace` -Expected: all suites pass; no pre-existing golden metric changes (this cycle touches no engine/CLI hot path). - -- [ ] **Lint** - -Run: `cargo clippy --workspace --all-targets -- -D warnings` -Expected: clean (no warnings). diff --git a/docs/specs/0073-instrument-geometry-crosscheck.md b/docs/specs/0073-instrument-geometry-crosscheck.md deleted file mode 100644 index 107fe9a..0000000 --- a/docs/specs/0073-instrument-geometry-crosscheck.md +++ /dev/null @@ -1,278 +0,0 @@ -# Consume DataServer instrument geometry: cross-check the vetted floor + two deploy fields — Design Spec - -**Date:** 2026-06-25 -**Status:** Draft — awaiting user spec review -**Authors:** orchestrator + Claude - -> **Reference issue:** Brummel/Aura #143 (consumer side) ⊕ data-server #3 (upstream -> reader, CLOSED). Scope fork resolved in the #143 reconciliation comment (cycle -> 0073): this cycle delivers geometry *consumption* + *cross-check* + two *additive* -> fields; it builds **no** runtime resolver and does **not** change -> `InstrumentSpec.quote_currency`'s type — both remain #124's. - -## Goal - -The data path now writes a per-symbol geometry sidecar (`.meta.json`) -beside the bars, and data-server #3 (CLOSED) shipped a reader that parses it into -neutral, broker-agnostic `InstrumentGeometry`. This cycle makes that geometry a -first-class, broker-agnostic input at aura's ingestion edge and uses it to -**cross-check the hand-authored vetted `InstrumentSpec` floor (#113) against -provider truth**, catching a typo in the authored table that would otherwise -silently mis-size every position. It also adds the two deploy-grade price-rounding -fields (`tick_size`, `digits`) the floor deferred, now that a provider source -supplies them. - -It deliberately does **not** build the four-tier runtime resolver or widen the -`quote_currency` type — those are #124's, and no broker/resolver consumer exists -yet (#116 / #120–123 open). "quote_currency beyond FX" is satisfied at the -geometry tier (`InstrumentGeometry.quote: String`), not by mutating the authored -floor's `&'static str` field. - -## Architecture - -Three changes, all at the `aura-ingest` ingestion edge (`crates/aura-ingest/src/lib.rs`), -plus a new integration test and a `Cargo.lock` bump: - -1. **Tier-2 source access.** Re-export `data_server::meta::InstrumentGeometry` and - add a thin, documented accessor `instrument_geometry(server, symbol)` over the - reader's `DataServer::symbol_meta`. This mirrors the crate's existing - `pub use data_server::{DataServer, DEFAULT_DATA_PATH}` discipline: a consumer - builds from `aura-ingest` alone and never names `data_server`, and the accessor - is the single vetted home where the C15 / untrusted-input / refuse-don't-guess - semantics of the recorded-metadata tier are stated. It surfaces the tier-2 - *source*; the tier-composing, authored-override-wins *resolver* stays #124. - -2. **Two additive `InstrumentSpec` fields.** `tick_size: f64` and `digits: u32`, - appended to the struct (it stays `Copy` — both are `Copy` scalars), authored for - the 5 vetted symbols from the verified sidecars. Additive per C15 - ("extensible without a signature break"). The construction sites the compiler - enumerates — the `instrument_spec()` table and the `instrument_spec_lookup_by_symbol` - unit test's 5 full literals — are updated in lock-step (a compile gate, not a - behaviour change). - -3. **Cross-check test.** A new integration test asserts, for every vetted symbol, - that the authored floor agrees with the provider geometry on `pip_size`, - `contract_size == lot_size`, `pip_value_per_lot == lot_size * pip_size` (the C10 - pip-value invariant against broker truth), `tick_size`, `digits`, and - `quote_currency`. It iterates the vetted set, so when #140 registers - NAS100 / USDJPY / USDCHF / NZDUSD the same test validates them for free. It - skips cleanly where the local Pepperstone archive is absent (hermetic - elsewhere), exactly as the existing real-data tests do. - -The `Cargo.lock` bump (`data-server` `0f5e665` → `694f96f2`) pulls the commit -carrying the reader; it is part of this cycle. - -## Concrete code shapes - -### Worked consumer example (the acceptance-criterion evidence) - -The audience is a downstream broker / deploy-step author building on `aura-ingest`. -This is the code they write — and what the cycle must make compile and run: - -```rust -use aura_ingest::{default_data_server, instrument_geometry, instrument_spec}; - -let server = default_data_server(); -let symbol = "EURUSD"; - -// The authored floor a broker sizes against — now with deploy-grade rounding inputs. -let spec = instrument_spec(symbol).expect("vetted symbol, or refuse-don't-guess"); -let price_decimals: u32 = spec.digits; // NEW: round a price for the order ticket -let tick: f64 = spec.tick_size; // NEW: snap a stop/limit to the tradable grid - -// The recorded provider geometry (tier 2 of #124), broker-agnostic, never raw JSON. -if let Some(geo) = instrument_geometry(&server, symbol) { - // Authoring/cross-check aid: provider truth for the same symbol. - debug_assert!((geo.lot_size * geo.pip_size - spec.pip_value_per_lot).abs() < 1e-9); - // Non-FX quote currency is available here as an owned String (geometry tier), - // without widening the &'static str authored floor (that type-fork is #124). - let _quote_ccy: &str = &geo.quote; -} -``` - -### The cross-check test the cycle delivers (`crates/aura-ingest/tests/instrument_geometry_crosscheck.rs`) - -```rust -//! 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, DEFAULT_DATA_PATH}; - -/// The vetted symbols. Kept in lock-step with the authored table; #140 extends it. -const VETTED: [&str; 5] = ["GER40", "FRA40", "EURUSD", "GBPUSD", "USDCAD"]; - -#[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 { - 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, - "{sym}: authored quote_currency {} != provider {}", spec.quote_currency, geo.quote); - } -} -``` - -### `InstrumentSpec` — before → after (additive, stays `Copy`) - -```rust -// BEFORE (7 fields) -pub struct InstrumentSpec { - pub instrument_id: i64, - pub pip_size: f64, - pub contract_size: f64, - pub pip_value_per_lot: f64, - pub min_lot: f64, - pub lot_step: f64, - pub quote_currency: &'static str, -} - -// AFTER (9 fields — two appended; quote_currency type unchanged) -pub struct InstrumentSpec { - pub instrument_id: i64, - pub pip_size: f64, - pub contract_size: f64, - pub pip_value_per_lot: f64, - pub min_lot: f64, - pub lot_step: f64, - 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, -} -``` - -### `instrument_spec()` table — authored values (from the verified sidecars) - -| symbol | tick_size | digits | -|--------|-----------|--------| -| GER40 | 0.1 | 1 | -| FRA40 | 0.1 | 1 | -| EURUSD | 1e-5 | 5 | -| GBPUSD | 1e-5 | 5 | -| USDCAD | 1e-5 | 5 | - -Each existing struct literal gains `tick_size: , digits: ,`. Example -(EURUSD): `… lot_step: 0.01, quote_currency: "USD", tick_size: 1e-5, digits: 5,`. - -### Re-export + accessor (`crates/aura-ingest/src/lib.rs`) - -```rust -// near the existing `pub use data_server::{DataServer, DEFAULT_DATA_PATH};` -/// Re-export of data-server's neutral instrument geometry (#143): a consumer reads -/// provider geometry through `aura-ingest` alone, never naming `data_server`. -pub use data_server::meta::InstrumentGeometry; - -// near `default_data_server` -/// Load the provider-recorded neutral geometry for `symbol` — the recorded -/// dataset-metadata tier (tier 2) of #124's resolution hierarchy — or `None` when -/// the dataset carries no usable sidecar (missing / unparseable / unsupported -/// version / a non-finite field; the reader absorbs every provider quirk). -/// -/// This 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`). Geometry is -/// non-scalar reference metadata held beside the hot path (C15); it is **not** a -/// `Source` and never enters the engine's `(Timestamp, Scalar)` seam. -pub fn instrument_geometry(server: &Arc, symbol: &str) -> Option { - server.symbol_meta(symbol) -} -``` - -## Components - -- **`crates/aura-ingest/src/lib.rs`** — the re-export, the `instrument_geometry` - accessor, the two new `InstrumentSpec` fields, the five updated table literals, - and the updated `instrument_spec_lookup_by_symbol` unit-test literals (compile - gate). The `instrument_spec_pip_value_matches_contract_times_pip` and - `instrument_spec_ids_are_distinct` unit tests are unaffected. -- **`crates/aura-ingest/tests/instrument_geometry_crosscheck.rs`** — new; the - cross-check above. -- **`crates/aura-ingest/tests/instrument_spec_deploy_metadata.rs`** — extend the - `every_vetted_symbol_resolves_to_a_complete_spec` property to also assert the two - new fields are sane (`tick_size > 0.0`, `digits > 0`), keeping the public-boundary - "no field left at a placeholder" guarantee complete. -- **`Cargo.lock`** — `data-server` bumped to `694f96f2` (carries the reader). - -## Data flow - -`.meta.json` on disk → (data-server, broker-facing) `symbol_meta` → -neutral `InstrumentGeometry` → (aura-ingest edge) `instrument_geometry` accessor → -consumer / cross-check test. The raw cTrader JSON never crosses into this repo; the -parser stays `pub(crate)` upstream. Geometry is keyed by symbol and held beside the -hot path — it never becomes a streamed scalar, and the engine's Source seam is -untouched (C15, invariant #4). - -## Error handling - -- **Refuse-don't-guess preserved.** `instrument_spec(unknown)` still returns `None`; - the geometry accessor is a *source for authoring/cross-check*, never a runtime - fallback that fabricates a spec for an unvetted symbol. -- **Untrusted input.** `instrument_geometry` returns `None` for a missing / - malformed / unsupported-version / non-finite sidecar (the reader's contract). The - cross-check test treats a *present archive with a missing vetted sidecar* as a - real failure (panic), but treats a *wholly absent archive* as skip — matching the - existing real-data tests. -- **No silent currency widening.** `quote_currency` stays `&'static str`; the - String-typed provider currency lives only in `InstrumentGeometry`. The cross-check - compares them for the vetted set (they agree), but does not fold the String into - the authored floor — that type transition is #124's. - -## Testing strategy - -- **Cross-check (new, real-data, skip-if-absent):** `authored_floor_agrees_with_provider_geometry` - — the headline. Verified to pass against the 30 live sidecars (all five vetted - symbols agree; the invariant `lot_size * pip_size == pip_value_per_lot` holds). -- **Compile gate:** the two new fields force the `instrument_spec()` table and the - unit-test literals to be updated together; `cargo build -p aura-ingest` is the - gate. -- **Extended public-boundary property:** `every_vetted_symbol_resolves_to_a_complete_spec` - gains `tick_size`/`digits` sanity asserts. -- **Regression:** all existing aura-ingest unit + integration tests stay green; no - golden metric anywhere else changes (this cycle touches no engine/CLI hot path). -- **Final gates:** `cargo test --workspace`, `cargo clippy --workspace --all-targets -- -D warnings`. - -## Acceptance criteria - -Against aura's default criterion ("solves a real user-facing problem and -contradicts no stated design commitment"): - -- **Real problem.** A downstream broker/deploy author reaches for `instrument_geometry` - to read broker-agnostic geometry and for `spec.tick_size` / `spec.digits` to round - prices to the tradable grid (the worked example above). The cross-check converts a - silent table typo — which would mis-size every position — into a build/test-time - failure against broker truth. -- **No commitment contradicted.** Determinism (C1), SoA/owned state (C7), one - output per node (C8), reference-metadata-beside-the-hot-path + additive-without- - signature-break (C15), refuse-don't-guess, untrusted-input, and engine-domain- - freedom all hold; the Source seam still carries only `(Timestamp, Scalar)`. The - resolver and the `quote_currency` type-fork are explicitly left to #124. -- **Measurable.** The headline test passes against live geometry; an injected - one-digit typo in the authored table makes it fail (the value it adds).