@@ -0,0 +1,321 @@
|
||||
# Per-Instrument Pip Metadata — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/0054-per-instrument-pip.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run
|
||||
> this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** Add a typed per-instrument pip metadata channel (`InstrumentSpec` +
|
||||
`instrument_spec(symbol)`) and thread the looked-up pip through the CLI
|
||||
`aura run --real` path, refusing an un-specced instrument.
|
||||
|
||||
**Architecture:** A new `InstrumentSpec { pip_size }` + `instrument_spec(symbol)`
|
||||
static vetted lookup in `aura-ingest`. The CLI's `sample_harness` and
|
||||
`sim_optimal_manifest` gain a `pip_size: f64` parameter; `run_sample_real` looks
|
||||
up the pip by symbol *before any data access* (refusing on `None`) and threads it;
|
||||
every other (synthetic) caller passes the unchanged `0.0001`. `SimBroker` is
|
||||
untouched.
|
||||
|
||||
**Tech Stack:** `aura-ingest/src/lib.rs` (lookup), `aura-cli/src/main.rs` (CLI
|
||||
threading + refusal), `aura-cli/tests/cli_run.rs` (integration test via
|
||||
`std::process::Command` + `env!("CARGO_BIN_EXE_aura")`).
|
||||
|
||||
**Files this plan creates or modifies:**
|
||||
|
||||
- Modify: `crates/aura-ingest/src/lib.rs` — add `InstrumentSpec` + `instrument_spec`
|
||||
(insert at ~`:341`, after `default_data_server`); unit test in the existing
|
||||
`#[cfg(test)] mod tests` (`:342`).
|
||||
- Modify: `crates/aura-cli/src/main.rs` — `sample_harness` (`:73`) +
|
||||
`sim_optimal_manifest` (`:130`) signatures; `run_sample` (`:149`),
|
||||
`run_sample_real` (`:183`); the 10 synthetic callers; an in-file `#[cfg(test)]`
|
||||
manifest-pip unit test + gated GER40 honesty test.
|
||||
- Test: `crates/aura-cli/tests/cli_run.rs` — NON-gated refusal integration test
|
||||
(synthetic pin at `:35` untouched).
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `InstrumentSpec` + `instrument_spec` lookup (aura-ingest)
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-ingest/src/lib.rs` (add struct + fn ~`:341`; test in `mod tests` `:342`)
|
||||
|
||||
- [ ] **Step 1: Write the failing unit test**
|
||||
|
||||
In `crates/aura-ingest/src/lib.rs`, inside `#[cfg(test)] mod tests` (after the
|
||||
`use super::*;` at `:344`), add:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn instrument_spec_lookup_by_symbol() {
|
||||
// index points → pip 1.0
|
||||
assert_eq!(instrument_spec("GER40"), Some(InstrumentSpec { pip_size: 1.0 }));
|
||||
assert_eq!(instrument_spec("FRA40"), Some(InstrumentSpec { pip_size: 1.0 }));
|
||||
// 5-decimal FX major → pip 0.0001
|
||||
assert_eq!(instrument_spec("EURUSD"), Some(InstrumentSpec { pip_size: 0.0001 }));
|
||||
// un-specced → None (the honesty lever)
|
||||
assert_eq!(instrument_spec("NONEXISTENT"), None);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p aura-ingest instrument_spec_lookup_by_symbol`
|
||||
Expected: FAIL — compile error `cannot find function 'instrument_spec' in this scope`
|
||||
(and `cannot find type 'InstrumentSpec'`).
|
||||
|
||||
- [ ] **Step 3: Write minimal implementation**
|
||||
|
||||
In `crates/aura-ingest/src/lib.rs`, immediately after the `default_data_server`
|
||||
function (ends `:340`), insert:
|
||||
|
||||
```rust
|
||||
/// Per-instrument reference metadata held beside the hot path (C7/C15): non-scalar,
|
||||
/// never streamed. Minimal today — extend with tick size / digits / quote currency
|
||||
/// as later cycles need, without breaking this signature.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct InstrumentSpec {
|
||||
/// Price units per pip / point (> 0). The sim-optimal broker's divisor.
|
||||
pub pip_size: f64,
|
||||
}
|
||||
|
||||
/// 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 pip_size = match symbol {
|
||||
"GER40" | "FRA40" => 1.0,
|
||||
"EURUSD" | "GBPUSD" | "USDCAD" => 0.0001,
|
||||
_ => return None,
|
||||
};
|
||||
Some(InstrumentSpec { pip_size })
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p aura-ingest instrument_spec_lookup_by_symbol`
|
||||
Expected: PASS (1 test).
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Thread pip + refusal through the CLI (aura-cli)
|
||||
|
||||
This task changes two function signatures in `main.rs`; the crate does not compile
|
||||
until **all** call sites are threaded, so every caller is updated within this task
|
||||
and the compile gate (Step 9) closes it. RED-first via the refusal integration test
|
||||
(it compiles against today's binary and asserts the new message).
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-cli/src/main.rs`
|
||||
- Test: `crates/aura-cli/tests/cli_run.rs`
|
||||
|
||||
- [ ] **Step 1: Write the failing refusal integration test**
|
||||
|
||||
In `crates/aura-cli/tests/cli_run.rs`, add (mirroring the existing
|
||||
`std::process::Command` + `env!("CARGO_BIN_EXE_aura")` pattern):
|
||||
|
||||
```rust
|
||||
#[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.
|
||||
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("no vetted pip/instrument spec for symbol 'NONEXISTENT'"),
|
||||
"expected the per-instrument-pip refusal message, got: {stderr}"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p aura-cli --test cli_run run_real_unspecced_symbol_refuses_with_exit_2`
|
||||
Expected: FAIL — exit code is `2` but the stderr is today's
|
||||
`no local data for symbol 'NONEXISTENT'`, not the new
|
||||
`no vetted pip/instrument spec ...` refusal message (assertion fails).
|
||||
|
||||
- [ ] **Step 3: Change `sample_harness` signature**
|
||||
|
||||
In `crates/aura-cli/src/main.rs`, change the `sample_harness` definition (`:73`):
|
||||
|
||||
```rust
|
||||
fn sample_harness(pip_size: f64) -> (
|
||||
```
|
||||
|
||||
and inside its body change the broker construction (`:91`, `:100`):
|
||||
|
||||
```rust
|
||||
Box::new(SimBroker::new(pip_size)), // 4 sim-optimal broker
|
||||
```
|
||||
```rust
|
||||
SimBroker::builder(pip_size).schema().clone(),
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Change `sim_optimal_manifest` signature + body**
|
||||
|
||||
Change the `sim_optimal_manifest` definition (`:130`) to take `pip_size`:
|
||||
|
||||
```rust
|
||||
fn sim_optimal_manifest(
|
||||
params: Vec<(String, Scalar)>,
|
||||
window: (Timestamp, Timestamp),
|
||||
seed: u64,
|
||||
pip_size: f64,
|
||||
) -> RunManifest {
|
||||
```
|
||||
|
||||
and change the hand-built broker literal (`:142`) to a `format!`:
|
||||
|
||||
```rust
|
||||
broker: format!("sim-optimal(pip_size={pip_size})"),
|
||||
```
|
||||
|
||||
(`format!("{}", 0.0001_f64)` renders `0.0001`; `format!("{}", 1.0_f64)` renders `1`.)
|
||||
|
||||
- [ ] **Step 5: Thread `run_sample` (synthetic — pass `0.0001`)**
|
||||
|
||||
In `run_sample` (`:149`): the `sample_harness()` call (`:150`) becomes
|
||||
`sample_harness(0.0001)`, and the `sim_optimal_manifest(...)` call (`:163`) gains a
|
||||
trailing `0.0001` argument after its `0,` seed line:
|
||||
|
||||
```rust
|
||||
let (mut h, rx_eq, rx_ex) = sample_harness(0.0001);
|
||||
```
|
||||
```rust
|
||||
manifest: sim_optimal_manifest(
|
||||
vec![
|
||||
("sma_fast".to_string(), Scalar::i64(2)),
|
||||
("sma_slow".to_string(), Scalar::i64(4)),
|
||||
("exposure_scale".to_string(), Scalar::f64(0.5)),
|
||||
],
|
||||
window,
|
||||
0,
|
||||
0.0001,
|
||||
),
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Thread `run_sample_real` — lookup FIRST, refuse, pass the pip**
|
||||
|
||||
In `run_sample_real` (`:183`), insert the lookup as the very first statement
|
||||
(before the `sample_harness()` call), refuse on `None`, then pass `spec.pip_size`
|
||||
to both `sample_harness` (`:184`) and `sim_optimal_manifest` (`:226`):
|
||||
|
||||
```rust
|
||||
fn run_sample_real(symbol: &str, from_ms: Option<i64>, to_ms: Option<i64>) -> RunReport {
|
||||
// Per-instrument pip: look up BEFORE any data access, so an un-specced symbol
|
||||
// refuses without touching local data. Honest by construction (#22).
|
||||
let spec = match aura_ingest::instrument_spec(symbol) {
|
||||
Some(s) => s,
|
||||
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)");
|
||||
std::process::exit(2);
|
||||
}
|
||||
};
|
||||
let (mut h, rx_eq, rx_ex) = sample_harness(spec.pip_size);
|
||||
```
|
||||
|
||||
and the `sim_optimal_manifest(...)` call (`:226`) gains `spec.pip_size` after its
|
||||
`0,` seed line:
|
||||
|
||||
```rust
|
||||
manifest: sim_optimal_manifest(
|
||||
vec![
|
||||
("sma_fast".to_string(), Scalar::i64(2)),
|
||||
("sma_slow".to_string(), Scalar::i64(4)),
|
||||
("exposure_scale".to_string(), Scalar::f64(0.5)),
|
||||
],
|
||||
window,
|
||||
0,
|
||||
spec.pip_size,
|
||||
),
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Thread the remaining synthetic callers (all pass `0.0001`)**
|
||||
|
||||
These are the call sites `rustc` flags after Steps 3-4; every one is a synthetic
|
||||
path → pass `0.0001` (behaviour-preserving, no emitted-pip change). Apply:
|
||||
|
||||
`sample_harness` callers — `sample_harness()` → `sample_harness(0.0001)` at:
|
||||
- `:589` (`mc_family` per-draw closure)
|
||||
- `:946` (in-file `#[cfg(test)]` caller)
|
||||
|
||||
`sim_optimal_manifest` callers — add a trailing `0.0001` argument at:
|
||||
- `:379` one-liner: `sim_optimal_manifest(zip_params(&space, point), window, 0)` →
|
||||
`sim_optimal_manifest(zip_params(&space, point), window, 0, 0.0001)`
|
||||
- `:498` one-liner: `sim_optimal_manifest(zip_params(&space, point), window, 0)` →
|
||||
`sim_optimal_manifest(zip_params(&space, point), window, 0, 0.0001)`
|
||||
- `:520` one-liner: `sim_optimal_manifest(zip_params(&space, params), window, 0)` →
|
||||
`sim_optimal_manifest(zip_params(&space, params), window, 0, 0.0001)`
|
||||
- `:597` multi-line — after its `seed,` line, before the closing `)`, add `0.0001,`
|
||||
- `:853` multi-line — after its `0,` line, before the closing `)`, add `0.0001,`
|
||||
- `:954` multi-line — after its `seed,` line, before the closing `)`, add `0.0001,`
|
||||
|
||||
- [ ] **Step 8: Add the manifest-pip unit test + gated GER40 honesty test**
|
||||
|
||||
In `crates/aura-cli/src/main.rs`, inside the in-file `#[cfg(test)] mod` (alongside
|
||||
the existing `run_sample_real_streams_real_close_bars_deterministically`), add:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn sim_optimal_manifest_renders_per_instrument_pip() {
|
||||
let m = sim_optimal_manifest(vec![], (Timestamp(1), Timestamp(2)), 0, 1.0);
|
||||
assert_eq!(m.broker, "sim-optimal(pip_size=1)");
|
||||
let m2 = sim_optimal_manifest(vec![], (Timestamp(1), Timestamp(2)), 0, 0.0001);
|
||||
assert_eq!(m2.broker, "sim-optimal(pip_size=0.0001)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_real_ger40_uses_index_pip() {
|
||||
// Gated: needs local GER40 data. Mirrors the existing real-path test's skip.
|
||||
let server = data_server::DataServer::new(data_server::DEFAULT_DATA_PATH);
|
||||
if !server.has_symbol("GER40") {
|
||||
eprintln!("skip: no local GER40 data at {}", data_server::DEFAULT_DATA_PATH);
|
||||
return;
|
||||
}
|
||||
let report = run_sample_real("GER40", None, None);
|
||||
// The looked-up index pip (1.0) reaches the manifest — not the FX 0.0001.
|
||||
assert_eq!(report.manifest.broker, "sim-optimal(pip_size=1)");
|
||||
// Deterministic (C1): a second run yields the same report.
|
||||
let again = run_sample_real("GER40", None, None);
|
||||
assert_eq!(report.manifest.broker, again.manifest.broker);
|
||||
assert_eq!(report.metrics.total_pips, again.metrics.total_pips);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 9: Compile gate — workspace builds with 0 errors**
|
||||
|
||||
Run: `cargo build --workspace`
|
||||
Expected: finishes with 0 errors (the two signature changes + all 12 call sites
|
||||
thread cleanly).
|
||||
|
||||
- [ ] **Step 10: Run the new + regression tests**
|
||||
|
||||
Run: `cargo test -p aura-cli --test cli_run run_real_unspecced_symbol_refuses_with_exit_2`
|
||||
Expected: PASS (the refusal message now matches).
|
||||
|
||||
Run: `cargo test -p aura-cli sim_optimal_manifest_renders_per_instrument_pip`
|
||||
Expected: PASS.
|
||||
|
||||
Run: `cargo test -p aura-cli --test cli_run run_prints_json_and_exits_zero`
|
||||
Expected: PASS — the synthetic run still emits `sim-optimal(pip_size=0.0001)`
|
||||
(`cli_run.rs:35` unchanged).
|
||||
|
||||
Run: `cargo test -p aura-cli run_real_ger40_uses_index_pip`
|
||||
Expected: PASS (or a clean skip line if no local GER40 data).
|
||||
|
||||
- [ ] **Step 11: Full workspace test sweep (no regressions)**
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: PASS — including the aura-ingest seam tests `real_bars.rs` /
|
||||
`streaming_seam.rs` (AAPL.US at `0.0001`, built literally — unaffected by the CLI
|
||||
signature change).
|
||||
Reference in New Issue
Block a user