03285d1b47
Milestone-scope fieldtest (closing functional gate) for "The World —
parameter-space & sweep" (issues #30-#36). Three curated end-to-end scenarios
derived top-down from the milestone promise, driven from the public interface
only (ledger + specs + rustdoc; no crates/*/src read), built and run from HEAD
via a downstream consumer crate.
Delivery verdict: GREEN. The milestone delivers its promise.
- mw_1 single run bound by name: .with("sma_cross.fast", 2).bootstrap() binds
and runs (12 equity rows); diagnostics precise and knob-named (UnknownKnob /
AmbiguousKnob / KindMismatch).
- mw_2 named-axis sweep + compare: .axis(..).sweep(run) yields a 4-member,
ordered (odometer), disjoint, non-constant SweepFamily; ranking from public
fields. CLI (aura sweep / aura runs rank) delivers the same with no Rust.
- mw_3 structural-constant negative space (deliberate): SMA2-entry's
definitional `2` has no honest expression today -> grounds #55. Out of
milestone scope; not a blocker.
Findings: 0 bugs, 2 working, 3 friction, 1 spec_gap. Verified independently
(re-ran all three from HEAD; confirmed a plain SMA-cross emits sma_cross.length
twice and the shipped CLI sample carries the fast/slow ParamAlias overlays).
Friction (routed to the forward queue, none blocks the gate):
- the promised sma_cross.fast/.slow names are not free — they require
author-added ParamAlias overlays, doubly mandatory because the canonical
2-SMA cross also will not bootstrap without them (IndistinguishableFanIn);
- the sweep closure drops back to positional bind + manual name re-zip.
spec_gap -> #55: the wished-for structural-constant consumer code is recorded
in mw_3 as #55's acceptance evidence.
This commit is the milestone fieldtest itself; it closes no issue. Closing the
tracker milestone stays a deliberate manual act on this green gate.
186 lines
9.0 KiB
Rust
186 lines
9.0 KiB
Rust
// Milestone fieldtest — scenario 3: the NEGATIVE-SPACE probe (grounds #55).
|
|
//
|
|
// The "SMA2-entry" strategy: "the close of the last candle is greater than the
|
|
// high of the prior candle, with SMA(2) as the bias." The `2` is DEFINITIONAL —
|
|
// it is bound to the two-candle construction. Sweeping it to 5 does not give
|
|
// "the same strategy at another point"; it deforms it into a different strategy.
|
|
//
|
|
// This file tries to express that `2` HONESTLY as a downstream author and
|
|
// records where the public surface forces a wrong shape. NOTHING is worked
|
|
// around: no `.bind()` is invented, no engine is touched. The deliverable is the
|
|
// verbatim friction plus the consumer code I WISH I could write (see the
|
|
// `WISHED-FOR` block at the bottom — it does not compile and is commented out).
|
|
//
|
|
// Public interface only: ledger (C8/C19/C23, C20) + specs 0015/0030 + rustdoc.
|
|
|
|
use std::sync::mpsc;
|
|
|
|
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
|
|
use aura_engine::{
|
|
BlueprintNode, Composite, Edge, OutField, ParamAlias, Role, Target,
|
|
};
|
|
use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub};
|
|
|
|
fn synthetic_prices() -> Vec<(Timestamp, Scalar)> {
|
|
let prices = [
|
|
1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07,
|
|
];
|
|
prices
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p)))
|
|
.collect()
|
|
}
|
|
|
|
// ===========================================================================
|
|
// AVENUE (a): leave the definitional length as an OPEN param_space() knob and
|
|
// fix it to 2 in the bind vector.
|
|
//
|
|
// The cross uses two Smas. The "bias" Sma is DEFINITIONALLY SMA(2) — but
|
|
// `Sma::builder()` is value-empty: its length is ALWAYS an injected param. So
|
|
// the `2` MUST surface as an open param-space knob. There is no way to bake it.
|
|
// ===========================================================================
|
|
fn sma2_entry_avenue_a() -> Composite {
|
|
Composite::new(
|
|
"sma2_entry",
|
|
vec![
|
|
Sma::builder().into(), // the DEFINITIONAL SMA(2) bias — but it is an OPEN knob
|
|
Sma::builder().into(), // a genuinely-tunable slow leg
|
|
Sub::builder().into(),
|
|
],
|
|
vec![
|
|
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
|
|
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
|
|
],
|
|
vec![Role {
|
|
name: "price".into(),
|
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
|
|
source: None,
|
|
}],
|
|
// ALIASES are MANDATORY here: Sub is a fan-in of two same-signature SMAs,
|
|
// each carrying an unaliased param slot -> C9 IndistinguishableFanIn at
|
|
// compile unless aliased. So even avenue (a) is forced to NAME the
|
|
// definitional-2 knob `bias` as if it were tunable.
|
|
vec![
|
|
ParamAlias { name: "bias".into(), node: 0, slot: 0 },
|
|
ParamAlias { name: "slow".into(), node: 1, slot: 0 },
|
|
],
|
|
vec![OutField { node: 2, field: 0, name: "out".into() }],
|
|
)
|
|
}
|
|
|
|
fn harness_with(inner: Composite, tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>) -> Composite {
|
|
Composite::new(
|
|
"harness",
|
|
vec![
|
|
BlueprintNode::Composite(inner),
|
|
Exposure::builder().into(),
|
|
SimBroker::builder(1e-4).into(),
|
|
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(),
|
|
],
|
|
vec![
|
|
Edge { from: 0, to: 1, slot: 0, from_field: 0 },
|
|
Edge { from: 1, to: 2, slot: 0, from_field: 0 },
|
|
Edge { from: 2, to: 3, slot: 0, from_field: 0 },
|
|
],
|
|
vec![Role {
|
|
name: "price".into(),
|
|
targets: vec![Target { node: 0, slot: 0 }, Target { node: 2, slot: 1 }],
|
|
source: Some(ScalarKind::F64),
|
|
}],
|
|
vec![],
|
|
vec![],
|
|
)
|
|
}
|
|
|
|
fn main() {
|
|
// --- AVENUE (a): the definitional 2 leaks into param_space() as a knob. ---
|
|
let (tx0, _rx0) = mpsc::channel();
|
|
let space = harness_with(sma2_entry_avenue_a(), tx0).param_space();
|
|
println!("AVENUE (a) — param_space() of the SMA2-entry strategy:");
|
|
for p in &space {
|
|
println!(" {} : {:?}", p.name, p.kind);
|
|
}
|
|
println!(
|
|
"\n >> The DEFINITIONAL SMA(2) length surfaces as `sma2_entry.bias` — an OPEN\n \
|
|
>> knob, indistinguishable from the genuinely-tunable `sma2_entry.slow`.\n \
|
|
>> A sweep `.axis(\"sma2_entry.bias\", [2, 5, 10])` would enumerate DEFORMED\n \
|
|
>> strategies (SMA-5-entry, SMA-10-entry) as valid family members.\n \
|
|
>> param_space() declares a degree of freedom the strategy's definition\n \
|
|
>> forbids. (C20: structural axes are NOT sweep params — but nothing here\n \
|
|
>> distinguishes the two.) NOTE: the alias `bias` was FORCED on this knob\n \
|
|
>> just to clear C9 IndistinguishableFanIn — so the surface makes me NAME\n \
|
|
>> the constant as if it were tunable."
|
|
);
|
|
|
|
// It still runs — the point is the SHAPE, not a crash:
|
|
let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
|
|
let mut h = harness_with(sma2_entry_avenue_a(), tx)
|
|
.bootstrap_with_params(vec![Scalar::I64(2), Scalar::I64(4), Scalar::F64(0.5)])
|
|
.expect("avenue (a) bootstraps");
|
|
h.run(vec![synthetic_prices()]);
|
|
drop(h);
|
|
let rows: Vec<_> = rx.iter().collect();
|
|
println!("\n (avenue (a) runs: {} recorded rows — it WORKS, but the 2 is fungible)", rows.len());
|
|
|
|
// --- AVENUE (b): imitate SimBroker::builder(pip_size) — a value captured
|
|
// OUTSIDE param_space — for the SMA length. ---------------------------
|
|
println!("\nAVENUE (b) — capture the 2 as a construction constant (SimBroker pattern):");
|
|
println!(
|
|
" SimBroker::builder(1e-4) bakes pip_size OUTSIDE param_space (it is C10/C15\n \
|
|
metadata — the broker declares NO param). The SMA2-entry author wants the\n \
|
|
SAME for the length: a value baked at build, never a knob.\n"
|
|
);
|
|
// Probe: is that pattern reachable for an INDICATOR node?
|
|
// Sma::builder() -> takes NO args; length is ALWAYS an injected param.
|
|
// Sma::new(2) -> exists, bakes length 2... but is it a BlueprintNode?
|
|
//
|
|
// `Sma::new(2)` returns an `Sma` (a built Node), NOT a `PrimitiveBuilder`.
|
|
// A composite's interior is `Vec<BlueprintNode>` = Primitive(PrimitiveBuilder)
|
|
// | Composite. There is NO `BlueprintNode::from(Sma)` for a built node — the
|
|
// blueprint holds value-empty RECIPES, not built nodes. So a built `Sma::new(2)`
|
|
// cannot be placed into a blueprint at all. The SimBroker escape hatch
|
|
// (`builder(constant)`) exists ONLY because SimBroker::builder TAKES the
|
|
// constant; Sma::builder() does not, and there is no `Sma::builder_with(2)`.
|
|
//
|
|
// The following line, if uncommented, does NOT compile — recorded as the
|
|
// verbatim wall (no `From<Sma> for BlueprintNode`):
|
|
//
|
|
// let _node: BlueprintNode = Sma::new(2).into();
|
|
//
|
|
println!(
|
|
" >> WALL: `Sma::builder()` takes no length arg (length is always a param),\n \
|
|
>> and `Sma::new(2)` returns a built `Sma`, not a `PrimitiveBuilder` /\n \
|
|
>> `BlueprintNode` — a blueprint holds value-empty RECIPES, not built nodes.\n \
|
|
>> There is no `Sma::builder_with(2)` analogous to `SimBroker::builder(pip)`.\n \
|
|
>> The construction-constant escape hatch exists for SimBroker (its builder\n \
|
|
>> TAKES the constant) but is UNREACHABLE for a stock indicator node."
|
|
);
|
|
|
|
// ===================================================================
|
|
// WISHED-FOR consumer code (the acceptance evidence for #55). This does
|
|
// NOT compile against today's surface; it is the code I reached for and
|
|
// could not write. Left here verbatim, commented out, as the empirical
|
|
// grounding for #55's future spec.
|
|
// ===================================================================
|
|
//
|
|
// // (1) An indicator builder that BAKES a structural constant, removing it
|
|
// // from param_space entirely (the opposite of `.with`, reserved `.bind`):
|
|
// let bias = Sma::builder().bind("length", 2); // length is now a CONSTANT, not a knob
|
|
//
|
|
// // ...so param_space() of the strategy contains ONLY the genuinely-tunable
|
|
// // knobs, and a sweep CANNOT enumerate a deformed SMA-5-entry:
|
|
// // param_space() == ["sma2_entry.slow": I64, "scale": F64] // no `length`
|
|
//
|
|
// // (2) equivalently, a recipe-level constructor that takes the constant:
|
|
// let bias = Sma::builder_with(2); // length baked at build, no param slot
|
|
//
|
|
// Either makes the definitional `2` a CONSTRUCTION value (C19 "params size
|
|
// nodes but a topology change is a different blueprint"; C20 "structural axes
|
|
// are not numeric sweep params"), distinguishing it from a tunable knob — which
|
|
// is exactly the distinction the SMA2-entry strategy needs and today's surface
|
|
// cannot express.
|
|
|
|
println!("\nOK: scenario 3 recorded both wrong avenues + the wished-for code (grounds #55).");
|
|
}
|