// 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)>) -> 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)>(); 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` = 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 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)."); }