feat(broker-foundation): InstrumentSpec deploy metadata + PositionEvent schema
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
This commit is contained in:
@@ -62,7 +62,8 @@ pub use harness::{
|
||||
VecSource,
|
||||
};
|
||||
pub use report::{
|
||||
f64_field, join_on_ts, summarize, ColumnarTrace, JoinedRow, RunManifest, RunMetrics, RunReport,
|
||||
f64_field, join_on_ts, summarize, ColumnarTrace, JoinedRow, PositionAction, PositionEvent,
|
||||
RunManifest, RunMetrics, RunReport,
|
||||
};
|
||||
pub use sweep::{
|
||||
sweep, GridSpace, ParamRange, RandomSpace, Space, SweepError, SweepFamily, SweepPoint,
|
||||
|
||||
@@ -29,6 +29,55 @@ pub struct RunMetrics {
|
||||
pub exposure_sign_flips: u64,
|
||||
}
|
||||
|
||||
/// The three position-event actions (C10). Direction IS the action; volume is
|
||||
/// unsigned. Serde-encoded as its i64 mapping (`Buy=0, Sell=1, Close=2`) so the
|
||||
/// persisted/columnar form stays C7-scalar and ledger-faithful (`action: i64`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(into = "i64", try_from = "i64")]
|
||||
pub enum PositionAction {
|
||||
Buy,
|
||||
Sell,
|
||||
Close,
|
||||
}
|
||||
|
||||
impl From<PositionAction> for i64 {
|
||||
fn from(a: PositionAction) -> i64 {
|
||||
match a {
|
||||
PositionAction::Buy => 0,
|
||||
PositionAction::Sell => 1,
|
||||
PositionAction::Close => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<i64> for PositionAction {
|
||||
type Error = String;
|
||||
fn try_from(v: i64) -> Result<Self, Self::Error> {
|
||||
match v {
|
||||
0 => Ok(PositionAction::Buy),
|
||||
1 => Ok(PositionAction::Sell),
|
||||
2 => Ok(PositionAction::Close),
|
||||
other => Err(format!("invalid PositionAction i64: {other}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One row of C10's derived position-event table — the broker-independent audit
|
||||
/// view (the first difference of the exposure state). A post-run value type
|
||||
/// (sibling of [`RunMetrics`]), NOT a per-`eval` node output (C8). Multiple events
|
||||
/// may share one `event_ts` (a reversal: Close then open, close-before-open). No
|
||||
/// `open_ts` — a position's open time is its opening event's `event_ts`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PositionEvent {
|
||||
pub event_ts: Timestamp,
|
||||
pub action: PositionAction,
|
||||
/// Monotonic, assigned at open. A Close references an existing `position_id`.
|
||||
pub position_id: i64,
|
||||
pub instrument_id: i64,
|
||||
/// Lots, unsigned. A partial close carries its own (smaller) volume.
|
||||
pub volume: f64,
|
||||
}
|
||||
|
||||
/// The reproducible run descriptor (C18). **Caller-supplied**: the engine
|
||||
/// cannot introspect a git commit, an RNG seed, or a broker label — the World
|
||||
/// that bootstraps and runs the harness fills these in.
|
||||
@@ -283,6 +332,57 @@ mod tests {
|
||||
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
|
||||
use std::sync::mpsc;
|
||||
|
||||
#[test]
|
||||
fn position_action_round_trips_through_i64() {
|
||||
for a in [PositionAction::Buy, PositionAction::Sell, PositionAction::Close] {
|
||||
let n: i64 = a.into();
|
||||
assert_eq!(PositionAction::try_from(n), Ok(a));
|
||||
}
|
||||
assert_eq!(i64::from(PositionAction::Buy), 0);
|
||||
assert_eq!(i64::from(PositionAction::Sell), 1);
|
||||
assert_eq!(i64::from(PositionAction::Close), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn position_action_rejects_out_of_range_i64() {
|
||||
assert!(PositionAction::try_from(3).is_err());
|
||||
assert!(PositionAction::try_from(-1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn position_event_serde_round_trips_with_bare_int_action() {
|
||||
let ev = PositionEvent {
|
||||
event_ts: Timestamp(42),
|
||||
action: PositionAction::Sell,
|
||||
position_id: 7,
|
||||
instrument_id: 3,
|
||||
volume: 0.5,
|
||||
};
|
||||
let json = serde_json::to_string(&ev).expect("serialize");
|
||||
// action encodes as a bare integer (C7 scalar shape), not a tagged enum
|
||||
assert!(json.contains("\"action\":1"), "action not bare-int encoded: {json}");
|
||||
let back: PositionEvent = serde_json::from_str(&json).expect("deserialize");
|
||||
assert_eq!(back, ev);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn position_events_may_share_one_event_ts_on_reversal() {
|
||||
// a stop-and-reverse: Close then open at the SAME event_ts (close-before-open).
|
||||
let ts = Timestamp(100);
|
||||
let close = PositionEvent {
|
||||
event_ts: ts, action: PositionAction::Close,
|
||||
position_id: 1, instrument_id: 3, volume: 0.5,
|
||||
};
|
||||
let open = PositionEvent {
|
||||
event_ts: ts, action: PositionAction::Sell,
|
||||
position_id: 2, instrument_id: 3, volume: 0.5,
|
||||
};
|
||||
let table = [close, open];
|
||||
assert_eq!(table[0].event_ts, table[1].event_ts);
|
||||
assert_eq!(table[0].action, PositionAction::Close);
|
||||
assert_eq!(table[1].action, PositionAction::Sell);
|
||||
}
|
||||
|
||||
/// The declared signature of a `Recorder` over one f64 column (the sink shape
|
||||
/// the two-sink harness uses).
|
||||
fn f64_recorder_sig() -> NodeSchema {
|
||||
|
||||
Reference in New Issue
Block a user