a56ab7859d
C28 phase 2 (Stratification); realizes item 1 of the deferred #147. The engine's production surface no longer names a backtest-metric type: - RunReport becomes generic over its metric payload M; sweep/mc/walkforward/ blueprint thread the parameter (SweepPoint<M>, SweepFamily<M>, WindowRun<M>, WalkForwardResult<M>). RunManifest stays concrete and engine-owned (its selection: Option<FamilySelection> embeds the foundation-grade analysis type). - summarize and the MC assembly (McDraw/McFamily/McAggregate/RBootstrap/ r_bootstrap/monte_carlo) move to aura-backtest - McAggregate::from_draws reads RunMetrics fields by name, so generifying it is the phase-6 metric-vocabulary abstraction (#147 item 2), still deferred; wholesale relocation is the honest cut. The concrete instantiation lives in aura-backtest as `type RunReport = aura_engine::RunReport<RunMetrics>` + sibling aliases. - the statistics kernel (MetricStats/quantile/resample_block/SplitMix64) moves to the aura-analysis foundation; the engine re-imports it (inner->foundation, legal) and re-exports it so existing consumers stay source-compatible. Dependency inversion in one commit: aura-engine drops aura-backtest from [dependencies] (back to dev-deps for its SimBroker/RunMetrics test fixtures); aura-backtest gains aura-engine. Cycle-free for lib targets - the cycle closes only through the engine's dev-dep edge, the pattern aura-vocabulary already uses. aura-backtest reaches the kernel transitively through the engine re-export, so no aura-backtest -> aura-analysis edge exists (the C28 ladder permits backtest -> {core, engine} only). run_indexed / SplitMix64::next_f64 widened pub(crate) -> pub for cross-crate use. Consumers (registry/campaign/cli/composites/ingest/bench) rewired by import path only, no call-site logic changed. The c28_layering structural test extends to the full ladder: aura-analysis (no aura-* deps), aura-engine ⊆ {core, analysis}, aura-backtest ⊆ {core, engine}. Behaviour-preserving: 1448/0 tests, clippy -D warnings clean, serde shapes byte-identical (C18 - RunReport<M> keeps field order manifest,metrics; the CLI pre-serialized-splice contract unchanged), moved code traceable via git rename detection. Cycle-introduced broken intra-doc links fixed. closes #292
89 lines
4.3 KiB
Rust
89 lines
4.3 KiB
Rust
//! End-to-end verification of the C10 derived position-event table (#114) at the
|
|
//! public crate boundary: the World consumes `PositionAction` / `PositionEvent`
|
|
//! via the `aura_backtest` re-exports (moved from `aura_engine`, #291) and
|
|
//! persists the audit table the same way the run registry persists a
|
|
//! `RunReport` — through serde_json. These tests pin the *consumer-visible
|
|
//! wire shape* (the persisted column form), distinct from the crate-internal
|
|
//! type-level unit tests in `aura-backtest::metrics`.
|
|
|
|
use aura_backtest::{PositionAction, PositionEvent};
|
|
use aura_engine::Timestamp;
|
|
|
|
/// A minimal stop-and-reverse audit table as a broker would derive it: open
|
|
/// long, then at one later instant Close-before-open into a short (a reversal,
|
|
/// two events sharing `event_ts`). The smallest table that exercises every
|
|
/// action variant and the same-ts reversal shape.
|
|
fn reversal_table() -> Vec<PositionEvent> {
|
|
let open = Timestamp(1_000);
|
|
let flip = Timestamp(2_000);
|
|
vec![
|
|
PositionEvent {
|
|
event_ts: open,
|
|
action: PositionAction::Buy,
|
|
position_id: 1,
|
|
instrument_id: 3,
|
|
volume: 0.10,
|
|
},
|
|
// reversal at one instant: close the long first, then open the short.
|
|
PositionEvent {
|
|
event_ts: flip,
|
|
action: PositionAction::Close,
|
|
position_id: 1,
|
|
instrument_id: 3,
|
|
volume: 0.10,
|
|
},
|
|
PositionEvent {
|
|
event_ts: flip,
|
|
action: PositionAction::Sell,
|
|
position_id: 2,
|
|
instrument_id: 3,
|
|
volume: 0.10,
|
|
},
|
|
]
|
|
}
|
|
|
|
/// Property: a derived position-event table survives a full serde_json
|
|
/// round-trip through the public re-exports byte-for-byte equal, AND the
|
|
/// persisted `action` column is a **bare integer** (C7-scalar / ledger column
|
|
/// shape), never a tagged-enum object. The World persists this audit table with
|
|
/// the same encoder it uses for `RunReport`; if `action` serialised as
|
|
/// `{"Sell":null}` it would break the flat columnar contract and any tool
|
|
/// reading the table as `action: i64`. Pins the consumer-visible on-disk shape.
|
|
#[test]
|
|
fn position_event_table_round_trips_with_bare_int_action_column() {
|
|
let table = reversal_table();
|
|
let json = serde_json::to_string(&table).expect("serialize audit table");
|
|
|
|
// action persists as a bare i64 (Buy=0, Sell=1, Close=2), not a tagged enum.
|
|
assert!(json.contains("\"action\":0"), "Buy must encode as action:0 — {json}");
|
|
assert!(json.contains("\"action\":1"), "Sell must encode as action:1 — {json}");
|
|
assert!(json.contains("\"action\":2"), "Close must encode as action:2 — {json}");
|
|
assert!(!json.contains("\"Buy\""), "action must not be a tagged enum — {json}");
|
|
|
|
let back: Vec<PositionEvent> = serde_json::from_str(&json).expect("deserialize audit table");
|
|
assert_eq!(back, table, "the audit table must round-trip byte-for-byte (C18 reproducibility)");
|
|
}
|
|
|
|
/// Property: the persisted `PositionAction` integer mapping is the **stable
|
|
/// wire encoding** `Buy=0, Sell=1, Close=2`, and any out-of-range integer is
|
|
/// **rejected** on read rather than silently coerced. This is the contract every
|
|
/// persisted audit table depends on: renumbering the variants (or accepting a
|
|
/// stray 3 as some action) would silently mis-read every historical table.
|
|
/// Exercised through the public re-export as a downstream reader would.
|
|
#[test]
|
|
fn position_action_wire_encoding_is_stable_and_validated() {
|
|
// forward: the literal i64 each variant persists as.
|
|
assert_eq!(serde_json::to_string(&PositionAction::Buy).unwrap(), "0");
|
|
assert_eq!(serde_json::to_string(&PositionAction::Sell).unwrap(), "1");
|
|
assert_eq!(serde_json::to_string(&PositionAction::Close).unwrap(), "2");
|
|
|
|
// backward: each valid code reads back to its variant.
|
|
assert_eq!(serde_json::from_str::<PositionAction>("0").unwrap(), PositionAction::Buy);
|
|
assert_eq!(serde_json::from_str::<PositionAction>("1").unwrap(), PositionAction::Sell);
|
|
assert_eq!(serde_json::from_str::<PositionAction>("2").unwrap(), PositionAction::Close);
|
|
|
|
// an out-of-range code is refused on read, not coerced (the validation arm).
|
|
assert!(serde_json::from_str::<PositionAction>("3").is_err());
|
|
assert!(serde_json::from_str::<PositionAction>("-1").is_err());
|
|
}
|