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:
2026-07-11 05:10:05 +02:00
parent 43b1c7ff5d
commit 7748b53a10
5 changed files with 328 additions and 2 deletions
+156
View File
@@ -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.