b91c89f964
The realistic-broker milestone's (C10 A-side) two foundation value types — no broker, no derivation yet, fully tested. #113 — InstrumentSpec gains six deploy-grade fields (instrument_id, contract_size, pip_value_per_lot, min_lot, lot_step, quote_currency) and the vetted table is populated for GER40/FRA40/EURUSD/GBPUSD/USDCAD. The struct stays Copy (quote_currency is &'static str); the _ => None refuse-don't-guess arm is the permanent floor. This is the milestone's permanent authored floor (recorded-metadata tier is forward work, #124). #114 — PositionAction { Buy, Sell, Close } (closed enum, serde-encoded as a bare i64 0/1/2 via From/TryFrom) + PositionEvent in aura-engine beside RunMetrics, faithful to the C10 column spec: event_ts, action, position_id, instrument_id, unsigned volume; no open_ts; direction is the action (no signed-volume trick). Re-exported from the crate root. Derived fork decisions (recorded on #113): quote_currency &'static str; action as ordinal i64; stable instrument_id + overridable vetted values. Verified by the orchestrator: cargo test --workspace = 458 passed / 0 failed; clippy --all-targets -D warnings clean. An adversarial 4-lens review (ledger faithfulness, downstream soundness, serde/wire robustness, test honesty) returned 3 SOUND + 1 CONCERN: quote_currency &'static str cannot hold a runtime-sourced currency at the #124 resolver — not a blocker (#115/#116 read only the authored floor), logged on #124 as a deliberate deferred design choice. The implementer's deviation from the plan snippet (vec![..] -> [..]) avoids clippy::useless_vec; semantics identical. Two tester-authored consumer-boundary E2E tests added beyond the inline units: instrument_spec_deploy_metadata.rs (public resolution seam) and position_event_table.rs (persisted bare-int wire shape + reversal table). closes #113 #114
87 lines
4.2 KiB
Rust
87 lines
4.2 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_engine` re-exports 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 `report.rs`.
|
|
|
|
use aura_engine::{PositionAction, PositionEvent, 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());
|
|
}
|