feat(cli,ingest): binding module + generalized column openers (#231 tasks 1-2)
crates/aura-cli/src/binding.rs: the closed column vocabulary (open/high/ low/close/spread/volume, price as the close alias) with ResolvedBinding — one ordered plan both halves of the single-price weld will share. resolve_binding applies override-wins-over-name-default, refuses unknown roles / bad override values / the price+close double-alias / a rebound reserved close role, synthesizes the broker-only close entry, and sorts into canonical M1Field order (the C4 merge tie-break contract). 12 exact-prose unit tests. Module-wide dead_code allow is scaffolding until the wrap seam consumes it next. aura-ingest: open_columns / open_columns_window open any field set in the given order (None if any field lacks an overlapping archive file); open_ohlc now delegates with the fixed OHLC order — behaviour-identical, still guarded by the open_ohlc_seam bit-identity test. Post-review polish: RESERVED_CLOSE_ROLE constant couples the synthesized close's two sites; dead_code note reworded condition-based. Verified: binding 12/12, ingest absence test green, full workspace suite green, clippy -D warnings clean; independent quality review approved. refs #231
This commit is contained in:
@@ -312,6 +312,53 @@ impl aura_engine::Source for M1FieldSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// Open one real [`M1FieldSource`] per requested field for `symbol` over
|
||||
/// `[from_ms, to_ms]` (inclusive Unix-ms, `None` = open-ended), in the GIVEN
|
||||
/// field order — callers pass the canonical `M1Field` declaration order (the
|
||||
/// C4 merge tie-break order, #92), so source index i is field i. All sources
|
||||
/// share the one `Arc<DataServer>` (one `FileCache`), so a window's bars are
|
||||
/// parsed once and reused across the field decodes (C12).
|
||||
///
|
||||
/// Returns `None` if any field has no archived file overlapping the window
|
||||
/// (the [`open_ohlc`] contract — file-level absence; since every archive file
|
||||
/// carries all fields, one absent field means all are). A window that
|
||||
/// overlaps a file but holds zero matching bars yields sources whose first
|
||||
/// `peek` is `None`, not a `None` here.
|
||||
pub fn open_columns(
|
||||
server: &Arc<DataServer>,
|
||||
symbol: &str,
|
||||
from_ms: Option<i64>,
|
||||
to_ms: Option<i64>,
|
||||
fields: &[M1Field],
|
||||
) -> Option<Vec<Box<dyn aura_engine::Source>>> {
|
||||
fields
|
||||
.iter()
|
||||
.map(|f| {
|
||||
M1FieldSource::open(server, symbol, from_ms, to_ms, *f)
|
||||
.map(|s| Box::new(s) as Box<dyn aura_engine::Source>)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// [`open_columns`] over an epoch-ns [`Timestamp`] window — the engine-side
|
||||
/// mirror, exactly as [`M1FieldSource::open_window`] mirrors
|
||||
/// [`M1FieldSource::open`]; the ns→ms crossing happens once, here.
|
||||
pub fn open_columns_window(
|
||||
server: &Arc<DataServer>,
|
||||
symbol: &str,
|
||||
from: Option<Timestamp>,
|
||||
to: Option<Timestamp>,
|
||||
fields: &[M1Field],
|
||||
) -> Option<Vec<Box<dyn aura_engine::Source>>> {
|
||||
open_columns(
|
||||
server,
|
||||
symbol,
|
||||
from.map(epoch_ns_to_unix_ms),
|
||||
to.map(epoch_ns_to_unix_ms),
|
||||
fields,
|
||||
)
|
||||
}
|
||||
|
||||
/// Open the four real OHLC [`M1FieldSource`]s for `symbol` over the closed
|
||||
/// epoch-ns window `[from, to]`, in the FIXED order open, high, low, close — the
|
||||
/// C4 merge tie-break order a resampler's `Barrier(0)` group depends on (#92).
|
||||
@@ -330,11 +377,13 @@ pub fn open_ohlc(
|
||||
from: Timestamp,
|
||||
to: Timestamp,
|
||||
) -> Option<Vec<Box<dyn aura_engine::Source>>> {
|
||||
let open = M1FieldSource::open_window(server, symbol, Some(from), Some(to), M1Field::Open)?;
|
||||
let high = M1FieldSource::open_window(server, symbol, Some(from), Some(to), M1Field::High)?;
|
||||
let low = M1FieldSource::open_window(server, symbol, Some(from), Some(to), M1Field::Low)?;
|
||||
let close = M1FieldSource::open_window(server, symbol, Some(from), Some(to), M1Field::Close)?;
|
||||
Some(vec![Box::new(open), Box::new(high), Box::new(low), Box::new(close)])
|
||||
open_columns_window(
|
||||
server,
|
||||
symbol,
|
||||
Some(from),
|
||||
Some(to),
|
||||
&[M1Field::Open, M1Field::High, M1Field::Low, M1Field::Close],
|
||||
)
|
||||
}
|
||||
|
||||
/// Construct the default data-server over the local archive at
|
||||
@@ -398,6 +447,24 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_columns_propagates_file_level_absence_as_none() {
|
||||
// The open_ohlc contract, generalized: file-level absence (unknown
|
||||
// symbol / no overlapping archive file) is None for the whole set —
|
||||
// hermetic (a bogus path has no archive whether or not local data
|
||||
// exists), like instrument_geometry_none_for_unknown_symbol above.
|
||||
let server = Arc::new(DataServer::new("/nonexistent-archive-path"));
|
||||
assert!(open_columns(&server, "NOSYM", None, None, &[M1Field::Close]).is_none());
|
||||
assert!(open_columns_window(
|
||||
&server,
|
||||
"NOSYM",
|
||||
Some(Timestamp(0)),
|
||||
Some(Timestamp(1)),
|
||||
&[M1Field::Open, M1Field::Close]
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
/// A hand-built M1 bar with distinct per-field values so a transpose test
|
||||
/// can tell the columns apart.
|
||||
fn full_bar(time_ms: i64) -> M1Parsed {
|
||||
|
||||
Reference in New Issue
Block a user