feat(cli,docs): the r_channel OHLC example + ledger amendment C26 (#231 tasks 6-7)
The acceptance proof: hl_channel — a causal Donchian channel (Delay(1) excludes the current bar, C2) consuming high/low/close roles, built from rostered vocabulary, shipped as the r_channel/r_channel_open example pair (builder + regenerator + serialize pins, the r_* pattern). Proven at three layers: a non-gated loaded-vs-carve equivalence over inline sources (non-zero-bias hardened per review), the unconditional synthetic multi-column refusal, and archive-gated CLI e2e — aura run on GER40 end-to-end plus a sweep whose members run and reproduce 2/2 bit-identically. Ledger: new C26 entry (the binding vocabulary — closed column set + price alias, name-driven default + campaign data.bindings override, canonical order = the C4 tie-break, Blockly-litmus argument, the #71 extension point for recorded non-price sources) and the C20/C24 scaffolding-clause annotations: the single-price data weld is retired, wrap_r's remaining R-scaffolding retirement stays #159. Verified: all proof tests green, full workspace suite green, clippy -D warnings clean; independent quality review approved. closes #231
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"hl_channel","nodes":[{"primitive":{"type":"Delay","name":"prev_high","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Delay","name":"prev_low","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"RollingMin","name":"channel_lo","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":4,"slot":1,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":4,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":0,"from_field":0},{"from":7,"to":8,"slot":1,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"},{"name":"close","targets":[{"node":4,"slot":0},{"node":5,"slot":1}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"hl_channel","nodes":[{"primitive":{"type":"Delay","name":"prev_high","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Delay","name":"prev_low","bound":[{"pos":0,"name":"lag","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"RollingMax","name":"channel_hi"}},{"primitive":{"type":"RollingMin","name":"channel_lo"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Gt"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Latch"}},{"primitive":{"type":"Sub"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":4,"slot":1,"from_field":0},{"from":3,"to":5,"slot":0,"from_field":0},{"from":4,"to":6,"slot":0,"from_field":0},{"from":5,"to":6,"slot":1,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":4,"to":7,"slot":1,"from_field":0},{"from":6,"to":8,"slot":0,"from_field":0},{"from":7,"to":8,"slot":1,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"},{"name":"close","targets":[{"node":4,"slot":0},{"node":5,"slot":1}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}],"gangs":[{"name":"channel_length","kind":"I64","members":[{"node":2,"pos":0,"name":"length"},{"node":3,"pos":0,"name":"length"}]}]}}
|
||||
@@ -2081,6 +2081,62 @@ fn r_meanrev_signal(window: Option<i64>, band_k: Option<f64>) -> Composite {
|
||||
g.build().expect("r_meanrev signal wiring resolves")
|
||||
}
|
||||
|
||||
/// The channel length for the canonical closed r_channel example. Single
|
||||
/// source for the emitter and the proof tests that must agree with the baked
|
||||
/// `examples/r_channel.json` (mirrors [`R_BREAKOUT_CHANNEL`]).
|
||||
#[cfg(test)]
|
||||
const R_CHANNEL_LENGTH: i64 = 3;
|
||||
|
||||
/// The OHLC high/low-channel (Donchian-shape) signal leg — the harness-input-
|
||||
/// binding acceptance strategy (#231): bias goes long when the CLOSE breaks
|
||||
/// the rolling max of the previous `n` HIGHS, short when it breaks the rolling
|
||||
/// min of the previous `n` LOWS. Three input roles (`high`, `low`, `close` —
|
||||
/// the role names ARE the column binding), declared in canonical column order.
|
||||
/// `channel = Some(n)` binds both rolling nodes (closed); `None` leaves them
|
||||
/// open, ganged into the single `channel_length` knob (#61). `#[cfg(test)]`:
|
||||
/// production loads the shipped JSON; this builder only regenerates + pins it
|
||||
/// (the r_breakout/r_meanrev carve pattern).
|
||||
#[cfg(test)]
|
||||
fn r_channel_signal(channel: Option<i64>) -> Composite {
|
||||
let mut g = GraphBuilder::new("hl_channel");
|
||||
let delay_hi = g.add(Delay::builder().named("prev_high").bind("lag", Scalar::i64(1)));
|
||||
let delay_lo = g.add(Delay::builder().named("prev_low").bind("lag", Scalar::i64(1)));
|
||||
let mut mx_b = RollingMax::builder().named("channel_hi");
|
||||
let mut mn_b = RollingMin::builder().named("channel_lo");
|
||||
if let Some(n) = channel {
|
||||
mx_b = mx_b.bind("length", Scalar::i64(n));
|
||||
mn_b = mn_b.bind("length", Scalar::i64(n));
|
||||
}
|
||||
let mx = g.add(mx_b);
|
||||
let mn = g.add(mn_b);
|
||||
if channel.is_none() {
|
||||
g.gang("channel_length", [mx.param("length"), mn.param("length")]);
|
||||
}
|
||||
let gt_up = g.add(Gt::builder());
|
||||
let gt_down = g.add(Gt::builder());
|
||||
let up_latch = g.add(Latch::builder());
|
||||
let down_latch = g.add(Latch::builder());
|
||||
let exposure = g.add(Sub::builder()); // up_latch - down_latch -> bias in {-1,0,+1}
|
||||
let high = g.source_role("high", ScalarKind::F64);
|
||||
let low = g.source_role("low", ScalarKind::F64);
|
||||
let close = g.source_role("close", ScalarKind::F64);
|
||||
g.feed(high, [delay_hi.input("series")]);
|
||||
g.feed(low, [delay_lo.input("series")]);
|
||||
g.feed(close, [gt_up.input("a"), gt_down.input("b")]);
|
||||
g.connect(delay_hi.output("value"), mx.input("series"));
|
||||
g.connect(delay_lo.output("value"), mn.input("series"));
|
||||
g.connect(mx.output("value"), gt_up.input("b"));
|
||||
g.connect(mn.output("value"), gt_down.input("a"));
|
||||
g.connect(gt_up.output("value"), up_latch.input("set"));
|
||||
g.connect(gt_down.output("value"), up_latch.input("reset"));
|
||||
g.connect(gt_down.output("value"), down_latch.input("set"));
|
||||
g.connect(gt_up.output("value"), down_latch.input("reset"));
|
||||
g.connect(up_latch.output("value"), exposure.input("lhs"));
|
||||
g.connect(down_latch.output("value"), exposure.input("rhs"));
|
||||
g.expose(exposure.output("value"), "bias");
|
||||
g.build().expect("hl_channel signal wiring resolves")
|
||||
}
|
||||
|
||||
/// Parse the `--params` value: a JSON array of externally-tagged `Scalar` cells
|
||||
/// (`[{"I64":2},{"F64":0.5}]`, the #155 wire form `Scalar` derives via serde). The cells
|
||||
/// bind positionally against the loaded signal's `param_space`. A malformed array is
|
||||
@@ -3446,6 +3502,106 @@ mod tests {
|
||||
.expect("write examples/r_meanrev_open.json");
|
||||
}
|
||||
|
||||
/// Regenerates the shipped r_channel examples from the carved signal. Run by hand:
|
||||
/// `cargo test --bin aura emit_r_channel_examples -- --ignored`. The examples are
|
||||
/// green-by-construction (serialised from the builder, never hand-authored).
|
||||
#[test]
|
||||
#[ignore = "regenerates crates/aura-cli/examples/r_channel{,_open}.json; run by hand"]
|
||||
fn emit_r_channel_examples() {
|
||||
std::fs::create_dir_all("examples").expect("examples dir");
|
||||
std::fs::write(
|
||||
"examples/r_channel.json",
|
||||
blueprint_to_json(&r_channel_signal(Some(R_CHANNEL_LENGTH))).expect("serialize closed r_channel"),
|
||||
)
|
||||
.expect("write examples/r_channel.json");
|
||||
std::fs::write(
|
||||
"examples/r_channel_open.json",
|
||||
blueprint_to_json(&r_channel_signal(None)).expect("serialize open r_channel"),
|
||||
)
|
||||
.expect("write examples/r_channel_open.json");
|
||||
}
|
||||
|
||||
/// The shipped examples are a faithful serialisation of the carved signal
|
||||
/// (the r_breakout pin pattern): re-serialising the carve equals the
|
||||
/// checked-in bytes; a drift between builder and file breaks it.
|
||||
#[test]
|
||||
fn shipped_r_channel_examples_serialize_the_carved_signal() {
|
||||
assert_eq!(
|
||||
blueprint_to_json(&r_channel_signal(Some(R_CHANNEL_LENGTH))).expect("serialize closed"),
|
||||
include_str!("../examples/r_channel.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
blueprint_to_json(&r_channel_signal(None)).expect("serialize open"),
|
||||
include_str!("../examples/r_channel_open.json"),
|
||||
);
|
||||
}
|
||||
|
||||
/// A 10-bar high/low/close fixture (canonical column order: high, low,
|
||||
/// close) that warms the hl_channel graph (Delay(1) + Rolling(3)) and
|
||||
/// breaks out upward then downward.
|
||||
fn hlc_sources() -> Vec<Box<dyn aura_engine::Source>> {
|
||||
let high = [10.5_f64, 10.6, 10.4, 10.8, 11.5, 12.0, 12.2, 11.0, 10.2, 9.8];
|
||||
let low = [9.5_f64, 9.7, 9.6, 9.9, 10.8, 11.4, 11.6, 10.1, 9.4, 9.0];
|
||||
let close = [10.0_f64, 10.2, 10.0, 10.5, 11.3, 11.8, 12.0, 10.4, 9.6, 9.2];
|
||||
[&high[..], &low[..], &close[..]]
|
||||
.into_iter()
|
||||
.map(|col| {
|
||||
let series: Vec<(Timestamp, Scalar)> = col
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &v)| (Timestamp(i as i64 + 1), Scalar::f64(v)))
|
||||
.collect();
|
||||
Box::new(VecSource::new(series)) as Box<dyn aura_engine::Source>
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The shipped closed example, reloaded through the data plane and run
|
||||
/// through the MULTI-COLUMN wrap, emits bit-identical bias rows to the
|
||||
/// carved signal — r_channel's durable equivalence anchor (the r_breakout
|
||||
/// idiom, over three VecSource columns instead of RunData::Synthetic,
|
||||
/// which honestly refuses multi-column signals). `wrap_r`'s ex tap reads
|
||||
/// `sig.output("bias")` directly, so `rx_ex` carries the raw signal bias.
|
||||
#[test]
|
||||
fn r_channel_example_loaded_runs_identically_to_the_carved_signal() {
|
||||
let run = |signal: Composite| -> Vec<(Timestamp, Vec<Scalar>)> {
|
||||
let binding =
|
||||
binding::resolve_binding(signal.name(), signal.input_roles(), &BTreeMap::new())
|
||||
.expect("high/low/close roles resolve");
|
||||
let (tx_eq, _rx_eq) = mpsc::channel();
|
||||
let (tx_ex, rx_ex) = mpsc::channel();
|
||||
let (tx_r, _rx_r) = mpsc::channel();
|
||||
let (tx_req, _rx_req) = mpsc::channel();
|
||||
let flat = wrap_r(
|
||||
signal,
|
||||
tx_eq,
|
||||
tx_ex,
|
||||
tx_r,
|
||||
tx_req,
|
||||
StopRule::Vol { length: 3, k: 2.0 },
|
||||
false,
|
||||
SYNTHETIC_PIP_SIZE,
|
||||
&binding,
|
||||
)
|
||||
.compile_with_params(&[])
|
||||
.expect("closed hl_channel wraps to a valid harness");
|
||||
let mut h = Harness::bootstrap(flat).expect("hl_channel harness bootstraps");
|
||||
h.run(hlc_sources());
|
||||
rx_ex.try_iter().collect()
|
||||
};
|
||||
let loaded = blueprint_from_json(include_str!("../examples/r_channel.json"), &|t| std_vocabulary(t))
|
||||
.expect("shipped r_channel example loads");
|
||||
let via_file = run(loaded);
|
||||
let via_carve = run(r_channel_signal(Some(R_CHANNEL_LENGTH)));
|
||||
assert!(!via_carve.is_empty(), "the channel must emit bias rows over the fixture");
|
||||
assert!(
|
||||
via_carve.iter().any(|(_, row)| row.iter().any(|s| s.as_f64() != 0.0)),
|
||||
"the fixture must drive a non-zero bias (an all-zero run would make \
|
||||
the equivalence vacuous)"
|
||||
);
|
||||
assert_eq!(via_file, via_carve, "loaded example emits the carve's bias rows");
|
||||
}
|
||||
|
||||
/// The shipped examples are a faithful serialisation of the carved signal (#159 cut 3):
|
||||
/// re-serialising the carve equals the checked-in bytes. Green-by-construction with the
|
||||
/// emitter; a drift between builder and file breaks it.
|
||||
|
||||
@@ -1010,6 +1010,125 @@ fn sweep_real_blueprint_member_manifest_stamps_project_provenance() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Acceptance (harness-input-binding spec, #231): an OHLC-consuming strategy
|
||||
/// blueprint runs end to end on real archive data through `aura run` — the
|
||||
/// role names bind columns, three real sources open in canonical order, the
|
||||
/// close column feeds broker/executor, and the report carries the R summary.
|
||||
/// Gated on the local GER40 archive; skips cleanly when absent.
|
||||
#[test]
|
||||
fn run_real_ohlc_channel_example_end_to_end() {
|
||||
if !local_data_present() {
|
||||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||||
return;
|
||||
}
|
||||
let cwd = temp_cwd("ohlc-run-real");
|
||||
let fixture = format!("{}/examples/r_channel.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(BIN)
|
||||
.args([
|
||||
"run", &fixture,
|
||||
"--real", "GER40",
|
||||
"--from", GER40_SEPT2024_FROM_MS,
|
||||
"--to", GER40_SEPT2024_TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("run report parses as JSON");
|
||||
assert!(
|
||||
v["manifest"]["topology_hash"].as_str().is_some(),
|
||||
"report carries the signal hash: {stdout}"
|
||||
);
|
||||
assert!(v["metrics"]["r"].is_object(), "the R summary is computed: {stdout}");
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// The synthetic walk generates a close series only: a multi-column blueprint
|
||||
/// without `--real` refuses honestly (exit 1, columns + remedy named). NOT
|
||||
/// gated — the refusal precedes any data access.
|
||||
#[test]
|
||||
fn run_synthetic_refuses_a_multi_column_blueprint() {
|
||||
let fixture = format!("{}/examples/r_channel.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = std::process::Command::new(BIN)
|
||||
.args(["run", &fixture])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(out.status.code(), Some(1));
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("consumes columns beyond close"), "names the shape: {stderr}");
|
||||
assert!(stderr.contains("--real"), "names the remedy: {stderr}");
|
||||
}
|
||||
|
||||
/// The dissolved real-data sweep + reproduce over the OHLC example (#231
|
||||
/// acceptance): `--list-axes` pins the ganged channel knob's wrapped axis
|
||||
/// name, the sweep runs one member per grid point over three real columns,
|
||||
/// and every member re-derives bit-identically from the store (#229 real-data
|
||||
/// reproduce, now multi-column). Gated on the local GER40 archive.
|
||||
#[test]
|
||||
fn sweep_real_ohlc_channel_members_run_and_reproduce() {
|
||||
if !local_data_present() {
|
||||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||||
return;
|
||||
}
|
||||
let (dir, _g) = fresh_project();
|
||||
let fixture = format!("{}/examples/r_channel_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let axes = std::process::Command::new(BIN)
|
||||
.args(["sweep", &fixture, "--list-axes"])
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&axes.stdout).trim(),
|
||||
"hl_channel.channel_length:I64",
|
||||
"the ganged channel knob is the one sweepable axis; stderr: {}",
|
||||
String::from_utf8_lossy(&axes.stderr)
|
||||
);
|
||||
let out = std::process::Command::new(BIN)
|
||||
.args([
|
||||
"sweep", &fixture,
|
||||
"--real", "GER40",
|
||||
"--from", GER40_SEPT2024_FROM_MS,
|
||||
"--to", GER40_SEPT2024_TO_MS,
|
||||
"--axis", "hl_channel.channel_length=3,5",
|
||||
"--name", "chan",
|
||||
])
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
|
||||
assert_eq!(stdout.lines().count(), 2, "one member line per channel length: {stdout}");
|
||||
for line in stdout.lines() {
|
||||
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
|
||||
assert_eq!(
|
||||
v["report"]["manifest"]["instrument"].as_str(),
|
||||
Some("GER40"),
|
||||
"campaign member manifest stamps the instrument: {line}"
|
||||
);
|
||||
}
|
||||
let family_id = serde_json::from_str::<serde_json::Value>(stdout.lines().next().unwrap())
|
||||
.expect("first member line parses")["family_id"]
|
||||
.as_str()
|
||||
.expect("member line carries family_id")
|
||||
.to_string();
|
||||
let rep = std::process::Command::new(BIN)
|
||||
.args(["reproduce", &family_id])
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
rep.status.success(),
|
||||
"reproduce stderr: {}",
|
||||
String::from_utf8_lossy(&rep.stderr)
|
||||
);
|
||||
assert!(
|
||||
String::from_utf8_lossy(&rep.stdout).contains("reproduced 2/2 members bit-identically"),
|
||||
"stdout: {}",
|
||||
String::from_utf8_lossy(&rep.stdout)
|
||||
);
|
||||
}
|
||||
|
||||
/// Property: the real-data blueprint sweep path (`aura sweep <blueprint.json>
|
||||
/// --real SYM --axis …`, dispatched as sugar over the one campaign executor)
|
||||
/// prints one member line per grid point, IN AXIS ORDER (odometer: the first
|
||||
|
||||
+51
-2
@@ -1726,7 +1726,11 @@ not a hand-coded blueprint nor engine-baked source: a structural axis selects am
|
||||
structural variation **first-class and searchable** rather than a hand-enumerated
|
||||
menu — `HarnessKind` and the per-strategy `*_sweep_family` functions in `aura-cli`
|
||||
are the pre-C24 scaffolding (topology-as-engine-source, a C16 tension), retired as
|
||||
C24 + the project-as-crate layer land.
|
||||
C24 + the project-as-crate layer land. [C26 realization, 2026-07-10 (#231): the
|
||||
scaffolding's last data weld — `wrap_r`'s hard-wired `price`←close role and the
|
||||
`M1Field::Close`-only open sites — is retired; a strategy's input roles now bind
|
||||
archive columns by name (C26). `wrap_r`'s remaining R-scaffolding retirement
|
||||
stays #159.]
|
||||
**Refinement (2026-07-03, #188/#189 — experiment INTENT is campaign-document data).**
|
||||
The "ordinary Rust control flow, not a config schema" clause and the forbids-clause
|
||||
"declarative experiment mini-DSL" are scoped by the #188 role-model pass: they forbid an
|
||||
@@ -2028,7 +2032,9 @@ authoring brain** (the format is required by C21/C18 at the CLI level, independe
|
||||
any UI); a **generated / searched run whose topology is not recoverable from its
|
||||
manifest** (C18 reproduction); baking topology as **engine source** compiled into the
|
||||
binary (the hard-wired `aura-cli` harnesses are pre-C24 scaffolding, a C16 tension,
|
||||
retired as this lands).
|
||||
retired as this lands). [C26, 2026-07-10 (#231): the single-price data weld inside
|
||||
the surviving `wrap_r` scaffolding is retired — input roles bind archive columns
|
||||
by name; the wrapper's remaining R-scaffolding retirement stays #159.]
|
||||
**Why.** The World (C21) is the product, and it cannot orchestrate, **structurally
|
||||
search**, or **reproduce** *families that vary topology* while topology stays opaque
|
||||
Rust source baked into the engine. The **game-engine principle** (C16's engine / game
|
||||
@@ -2324,6 +2330,49 @@ artifacts (process/campaign documents, C18 realization note); roles 4/7/8 remain
|
||||
technically present but faceless (no addressed verb families yet); role homes in
|
||||
the project layout and docs-by-role are open (#192 context).
|
||||
|
||||
### C26 — Harness input binding: role names bind archive columns
|
||||
**Guarantee.** A strategy blueprint declares WHICH data it consumes as the NAMES
|
||||
of its root input roles: a role whose name is in the **closed column
|
||||
vocabulary** — `open`, `high`, `low`, `close`, `spread`, `volume`, plus `price`
|
||||
as the backward-compatible alias of `close` — binds by default to that column of
|
||||
the campaign cell's (or run invocation's) instrument. A campaign document may
|
||||
**override** the name default per role via the additive `data.bindings` block
|
||||
(`role name → column name`; serde `default` + skip-if-empty, so binding-less
|
||||
documents keep their content ids) — the 6b rebind seam: the blueprint's content
|
||||
identity never changes. Resolution (`aura-cli`'s `binding` module,
|
||||
`resolve_binding`) produces ONE ordered plan consumed by BOTH halves of the old
|
||||
weld: the columns are opened (`aura_ingest::open_columns`, the generalization of
|
||||
`open_ohlc`/#92) and the wrapped root roles declared (`wrap_r`) in the **same
|
||||
canonical order** — `M1Field` declaration order (open, high, low, close, spread,
|
||||
volume), filtered to the consumed set — which is the C4 merge tie-break order,
|
||||
so role i receives source i by construction. A close column is always part of
|
||||
the plan (shared when the strategy consumes `close`/`price`, else opened for the
|
||||
broker/executor pair alone). The vocabulary is **Blockly-litmus-clean** (C25):
|
||||
role-name slots draw from a closed enum, the bindings block is typed
|
||||
`role → column` with no free-form holes, and validation is two-tier — values
|
||||
against the column vocabulary in the intrinsic tier (`validate_campaign`), keys
|
||||
against the strategies' `input_roles()` in the resolver tier
|
||||
(`validate_campaign_refs`).
|
||||
**Forbids.** Guessing a column for an unknown role name (refuse with the
|
||||
vocabulary + the override remedy, never default); opening columns in any order
|
||||
other than the canonical declaration order (the C4 tie-break contract);
|
||||
free-form or logic-bearing binding values (the C17/C25 line — a binding value is
|
||||
a typed column reference, nothing else); fabricating synthetic OHLC when a
|
||||
multi-column strategy meets synthetic data (the walk generates a close series
|
||||
only — refuse with the `--real` remedy).
|
||||
**Extension point.** When recorded non-price sources land (the #71 Source seam),
|
||||
the binding VALUE-space grows additively from archive columns to recorded-stream
|
||||
references — a root role like `sentiment` becomes bindable to a recorded stream
|
||||
then, and until then refuses with the vocabulary-naming message. The role-name
|
||||
key-space and the campaign `bindings` carrier are unchanged by that growth.
|
||||
**Why.** The single-price weld (`wrap_r`'s hard-wired `price`←close and the six
|
||||
`M1Field::Close`-only open sites) made every strategy monocular regardless of
|
||||
what its blueprint declared; binding by role name keeps the blueprint the single
|
||||
source of its own data needs (C24: topology-as-data carries its input contract),
|
||||
lets a campaign re-aim a strategy without touching its content id (C18
|
||||
identity), and pins opening and declaring to one shared, canonically-ordered
|
||||
plan so the two halves cannot drift (C1/C4 determinism).
|
||||
|
||||
---
|
||||
|
||||
## Open architectural threads not yet resolved
|
||||
|
||||
Reference in New Issue
Block a user