feat(0073): consume DataServer geometry — cross-check vetted floor + tick_size/digits

Make data-server's neutral InstrumentGeometry a first-class, broker-agnostic input at the aura-ingest edge: re-export InstrumentGeometry + a thin instrument_geometry(server, symbol) accessor over symbol_meta (the recorded dataset-metadata tier 2 of #124's hierarchy; the tier-composing resolver stays #124). Add the two deferred deploy-grade InstrumentSpec fields tick_size + digits (additive, struct stays Copy; C15), authored for the 5 vetted symbols from the verified sidecars. A new real-data cross-check asserts the authored floor agrees with provider geometry (pip_size, contract_size==lot_size, the C10 pip_value invariant, tick_size, digits, quote_currency), iterating the vetted set so #140's additions are covered for free; skip-if-archive-absent.

Scope (derived, recorded on #143): no runtime resolver and no quote_currency type change — both remain #124's. 'quote_currency beyond FX' is satisfied at the geometry tier (InstrumentGeometry.quote: String), not by widening the &'static str floor.

Beyond the plan (implementer additions, reviewed and accepted): hoisted the vetted-symbol list to one pub const VETTED_SYMBOLS, collapsing the three drift-prone copies plan-recon flagged (#140 now extends one constant); a tick_size == 10^(-digits) consistency guard (verified to hold across all 30 provider sidecars, so it will not false-trip on #140); a re-export-nameable seam test guarding the InstrumentGeometry re-export from silent removal.

Verification: cargo test --workspace all green; cargo clippy --workspace --all-targets -D warnings clean. The cross-check ran against the live Pepperstone archive (not skipped) — all 5 vetted sidecars match the authored table.

refs #143
This commit is contained in:
2026-06-25 16:25:51 +02:00
parent 80ba74460d
commit 5b7cd5eb2d
3 changed files with 170 additions and 9 deletions
+58
View File
@@ -28,6 +28,9 @@ use data_server::SymbolChunkIter;
/// builds from `aura-ingest` alone — a consumer never names the external
/// `data_server` crate directly.
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;
use std::sync::Arc;
/// Normalize data-server's Unix-millisecond time to aura's canonical epoch-ns
@@ -339,6 +342,22 @@ pub fn default_data_server() -> Arc<DataServer> {
Arc::new(DataServer::new(DEFAULT_DATA_PATH))
}
/// 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<DataServer>, symbol: &str) -> Option<InstrumentGeometry> {
server.symbol_meta(symbol)
}
/// Per-instrument reference metadata held beside the hot path (C7/C15): non-scalar,
/// never streamed. The permanent vetted floor of the milestone's
/// authored-override-wins resolution hierarchy (the recorded-metadata tier is
@@ -362,8 +381,21 @@ pub struct InstrumentSpec {
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.
@@ -376,22 +408,27 @@ pub fn instrument_spec(symbol: &str) -> Option<InstrumentSpec> {
"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,
};
@@ -402,6 +439,17 @@ pub fn instrument_spec(symbol: &str) -> Option<InstrumentSpec> {
mod tests {
use super::*;
#[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());
}
#[test]
fn instrument_spec_lookup_by_symbol() {
// index CFDs: quote EUR, 1 contract = €1/point, pip = 1.0 point
@@ -415,6 +463,8 @@ mod tests {
min_lot: 0.1,
lot_step: 0.01,
quote_currency: "EUR",
tick_size: 0.1,
digits: 1,
})
);
assert_eq!(
@@ -427,6 +477,8 @@ mod tests {
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
@@ -440,6 +492,8 @@ mod tests {
min_lot: 0.01,
lot_step: 0.01,
quote_currency: "USD",
tick_size: 1e-5,
digits: 5,
})
);
assert_eq!(
@@ -452,6 +506,8 @@ mod tests {
min_lot: 0.01,
lot_step: 0.01,
quote_currency: "USD",
tick_size: 1e-5,
digits: 5,
})
);
assert_eq!(
@@ -464,6 +520,8 @@ mod tests {
min_lot: 0.01,
lot_step: 0.01,
quote_currency: "CAD",
tick_size: 1e-5,
digits: 5,
})
);
// un-specced → None (the honesty lever)
@@ -0,0 +1,82 @@
//! 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<InstrumentGeometry> = 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
);
}
}
@@ -5,22 +5,19 @@
//! 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};
/// The vetted symbols a deploy step may ask for. Kept here (not imported) so the
/// test fails loudly if the public lookup ever stops resolving one of them.
const VETTED: [&str; 5] = ["GER40", "FRA40", "EURUSD", "GBPUSD", "USDCAD"];
use aura_ingest::{instrument_spec, InstrumentSpec, VETTED_SYMBOLS};
/// Property: every vetted symbol resolves through the *public* lookup to a fully
/// populated `InstrumentSpec` whose six deploy fields are all present and sane
/// (positive sizes, a non-empty quote currency). This is the floor a broker
/// 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 {
for sym in VETTED_SYMBOLS {
let s: InstrumentSpec = instrument_spec(sym).unwrap_or_else(|| {
panic!("vetted symbol {sym} must resolve through the public lookup")
});
@@ -31,6 +28,30 @@ fn every_vetted_symbol_resolves_to_a_complete_spec() {
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,
);
}
}
@@ -43,7 +64,7 @@ fn every_vetted_symbol_resolves_to_a_complete_spec() {
/// 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 {
for sym in VETTED_SYMBOLS {
let s = instrument_spec(sym).expect("vetted symbol");
let derived = s.contract_size * s.pip_size;
assert!(