Files
Aura/docs/specs/0073-instrument-geometry-crosscheck.md
T
Brummel 8d9939733d spec: 0073 instrument-geometry cross-check + deploy fields (boss-signed)
Consume data-server's neutral InstrumentGeometry at the aura-ingest edge to cross-check the hand-authored vetted InstrumentSpec floor (#113) against provider truth, and add the two deferred deploy-grade fields (tick_size, digits) the sidecars now supply.

Scope fork derived and recorded on #143: this cycle does NOT build #124's runtime resolver and does NOT change quote_currency's &'static str type (the geometry tier carries non-FX currency as String). Auto-signed autonomously under /boss on a grounding-check PASS (6/6 assumptions ratified); reply to the run to veto.

refs #143
2026-06-25 15:47:48 +02:00

279 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (`<SYMBOL>.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 / #120123 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: <v>, digits: <n>,`. 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<DataServer>, symbol: &str) -> Option<InstrumentGeometry> {
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
`<SYMBOL>.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).