feat(0074): source real-path pip from the geometry sidecar; drop the authored floor
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
This commit is contained in:
+28
-22
@@ -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<data_server::DataServer>, 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<i64>,
|
||||
to_ms: Option<i64>,
|
||||
) -> (Box<dyn aura_engine::Source>, (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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <SYMBOL>` 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
|
||||
/// <unspecced>` 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.
|
||||
/// <no-geometry symbol>` 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);
|
||||
}
|
||||
|
||||
@@ -224,9 +224,9 @@ pub fn summarize_r(record: &[(Timestamp, Vec<Scalar>)], 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<Scalar>)],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -349,8 +349,9 @@ pub fn default_data_server() -> Arc<DataServer> {
|
||||
///
|
||||
/// 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<DataServer>, symbol: &str) -> Option<Ins
|
||||
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
|
||||
/// forward work, #124). Stays `Copy` — `quote_currency` is `&'static str`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct InstrumentSpec {
|
||||
/// Stable per-instrument id — the position-event table's `instrument_id`
|
||||
/// column. Assigned once, never renumbered.
|
||||
pub instrument_id: i64,
|
||||
/// Price units per pip / point (> 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<InstrumentSpec> {
|
||||
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<i64> = 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.
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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<InstrumentGeometry> = instrument_geometry(&server, "EURUSD");
|
||||
}
|
||||
@@ -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<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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+30
-26
@@ -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<InstrumentSpec>`
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user