feat(aura): node signature lives in the blueprint; collapse Blueprint into Composite
Consolidate the node data structure so every node's signature (NodeSchema:
inputs/output/params) is declared once and exists in the blueprint pre-build,
and dissolve the special "root graph" type. Behaviour-preserving (C1).
Signature vs sizing
- NodeSchema is now the static signature only: InputSpec -> PortSpec{kind,firing},
with lookback removed. The signature is fully static per blueprint (input
kinds/firing, output fields, params); LinComb's variable arity is a builder arg,
not an injected param.
- The one param-dependent quantity, an input's buffer lookback (e.g. Sma's window =
its injected length), moves out of the signature to Node::lookbacks() -> Vec<usize>,
read only by bootstrap for sizing. Node::schema() is removed.
- LeafFactory -> PrimitiveBuilder, which carries the full NodeSchema. The built node
no longer re-declares it: closes the params-declared-twice drift (#36, the 8
per-node factory_params_match_built_node_schema lockstep tests are deleted — their
subject is now structurally impossible) and a value-empty recipe exposes its full
I/O interface pre-build (#43).
Root is just a bound composite
- struct Blueprint is deleted; its compile/bootstrap/param_space methods move onto
Composite. Role gains source: Option<ScalarKind> (None = open interior port,
Some = bound ingestion feed). A composite is runnable iff every root role is bound;
the "main graph" is no longer a category, only the fully-source-bound composite.
New error CompileError::UnboundRootRole for an open root role.
- BlueprintNode::signature() answers uniformly for both arms: Primitive returns the
builder's declared schema, Composite derives it from the interior (role kinds in,
OutField kinds out, aggregated params), pre-build, no build.
compile -> FlatGraph -> bootstrap
- compile validates structure pre-build via signature() (validate_wiring: range +
kind, returning the same variants as before, so an edge kind fault is now caught
before any build closure fires) and emits FlatGraph{nodes,signatures,sources,edges}.
- bootstrap consumes the FlatGraph: kinds/firing/output from the carried signatures,
buffer depth from node.lookbacks(). SourceSpec survives as the flat descriptor.
Renames: BlueprintNode::Leaf -> Primitive, LeafFactory -> PrimitiveBuilder.
Render (aura-cli/src/graph.rs) is migrated compile-only: it takes &Composite, maps
bound roles to the same source-entry shape, so both render goldens reproduce
byte-identical output (no re-capture needed). Render-fidelity tuning is the next cycle.
Verification (orchestrator-run, not agent-reported): cargo build --workspace green;
cargo test --workspace 150 passed / 0 failed; cargo clippy --workspace --all-targets
-D warnings clean. All pinned determinism/run-output tests pass with values unchanged;
no behavioural assertion was altered to go green. 5 new tests assert the signature is
pre-build and uniform, that compile rejects a kind mismatch without building (via a
panicking builder), UnboundRootRole, and lookbacks()/signature arity agreement.
Deferred to cycle-close audit (per plan): docs/design/INDEX.md and some aura-std
module docs still name the old Node::schema()/LeafFactory/BlueprintNode::Leaf/
Blueprint::param_space contracts; prose reconciliation is the architect's at audit.
closes #43 #36
This commit is contained in:
+17
-20
@@ -2,7 +2,7 @@
|
||||
//! Combines two signal streams into one — the most basic combinator for the
|
||||
//! north-star "combine one signal with another" research move (C10).
|
||||
|
||||
use aura_core::{Ctx, FieldSpec, Firing, InputSpec, LeafFactory, Node, NodeSchema, Scalar, ScalarKind};
|
||||
use aura_core::{Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, Scalar, ScalarKind};
|
||||
|
||||
/// Two-input f64 sum: input 0 plus input 1. Emits `None` until both inputs
|
||||
/// have a value.
|
||||
@@ -26,10 +26,21 @@ impl Add {
|
||||
Self { out: [Scalar::F64(0.0)] }
|
||||
}
|
||||
|
||||
/// The param-generic recipe for a blueprint leaf: paramless, builds through
|
||||
/// The param-generic recipe for a blueprint primitive: paramless, builds through
|
||||
/// `Add::new`.
|
||||
pub fn factory() -> LeafFactory {
|
||||
LeafFactory::new("Add", vec![], |_| Box::new(Add::new()))
|
||||
pub fn builder() -> PrimitiveBuilder {
|
||||
PrimitiveBuilder::new(
|
||||
"Add",
|
||||
NodeSchema {
|
||||
inputs: vec![
|
||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any },
|
||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any },
|
||||
],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
},
|
||||
|_| Box::new(Add::new()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,15 +51,8 @@ impl Default for Add {
|
||||
}
|
||||
|
||||
impl Node for Add {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: vec![
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any },
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any },
|
||||
],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1, 1]
|
||||
}
|
||||
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -71,13 +75,6 @@ mod tests {
|
||||
use super::*;
|
||||
use aura_core::{AnyColumn, Timestamp};
|
||||
|
||||
#[test]
|
||||
fn factory_params_match_built_node_schema() {
|
||||
let f = Add::factory();
|
||||
let built = f.build(&[]);
|
||||
assert_eq!(f.params(), built.schema().params.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_is_sum_once_both_inputs_present() {
|
||||
let mut add = Add::new();
|
||||
|
||||
+20
-24
@@ -18,7 +18,8 @@
|
||||
//! nothing on the hot path (the output buffer is reused).
|
||||
|
||||
use aura_core::{
|
||||
Ctx, FieldSpec, Firing, InputSpec, LeafFactory, Node, NodeSchema, ParamSpec, Scalar, ScalarKind,
|
||||
Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar,
|
||||
ScalarKind,
|
||||
};
|
||||
|
||||
/// Exponential moving average of one f64 input, smoothing `alpha = 2/(length+1)`,
|
||||
@@ -52,31 +53,27 @@ impl Ema {
|
||||
}
|
||||
}
|
||||
|
||||
/// The param-generic recipe for a blueprint leaf: declares `length` and builds
|
||||
/// The param-generic recipe for a blueprint primitive: declares `length` and builds
|
||||
/// through `Ema::new` (the single sizing/validation gate; the slice is
|
||||
/// kind-checked before `build` runs, so the typed read is total).
|
||||
pub fn factory() -> LeafFactory {
|
||||
LeafFactory::new(
|
||||
pub fn builder() -> PrimitiveBuilder {
|
||||
PrimitiveBuilder::new(
|
||||
"EMA",
|
||||
vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
NodeSchema {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any }],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
},
|
||||
|p| Box::new(Ema::new(p[0].as_i64().expect("length slot is I64") as usize)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for Ema {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: vec![InputSpec {
|
||||
kind: ScalarKind::F64,
|
||||
// recursive: the running average lives in internal state, so only
|
||||
// the newest sample is read — `length` sizes alpha, not the window.
|
||||
lookback: 1,
|
||||
firing: Firing::Any,
|
||||
}],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
}
|
||||
// recursive: the running average lives in internal state, so only the newest
|
||||
// sample is read — `length` sizes alpha, not the window.
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1]
|
||||
}
|
||||
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -157,12 +154,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_params_match_built_node_schema() {
|
||||
let f = Ema::factory();
|
||||
let built = f.build(&[Scalar::I64(9)]);
|
||||
assert_eq!(f.params(), built.schema().params.as_slice());
|
||||
// the built node carries the declared I64 `length` knob
|
||||
assert_eq!(built.schema().params[0].name, "length");
|
||||
assert_eq!(built.schema().params[0].kind, ScalarKind::I64);
|
||||
fn builder_declares_the_length_knob() {
|
||||
// the param-generic recipe carries the declared I64 `length` knob
|
||||
let b = Ema::builder();
|
||||
assert_eq!(b.params(), b.schema().params.as_slice());
|
||||
assert_eq!(b.schema().params[0].name, "length");
|
||||
assert_eq!(b.schema().params[0].kind, ScalarKind::I64);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
//! `scale` sets which signal magnitude maps to full exposure (sizing lives here).
|
||||
|
||||
use aura_core::{
|
||||
Ctx, FieldSpec, Firing, InputSpec, LeafFactory, Node, NodeSchema, ParamSpec, Scalar, ScalarKind,
|
||||
Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar,
|
||||
ScalarKind,
|
||||
};
|
||||
|
||||
/// Bounded exposure from a raw signal score: `clamp(signal / scale, -1.0, +1.0)`.
|
||||
@@ -21,25 +22,25 @@ impl Exposure {
|
||||
Self { scale, out: [Scalar::F64(0.0)] }
|
||||
}
|
||||
|
||||
/// The param-generic recipe for a blueprint leaf: declares `scale` and builds
|
||||
/// The param-generic recipe for a blueprint primitive: declares `scale` and builds
|
||||
/// through `Exposure::new` (the single sizing/validation gate; the slice is
|
||||
/// kind-checked before `build` runs, so the typed read is total).
|
||||
pub fn factory() -> LeafFactory {
|
||||
LeafFactory::new(
|
||||
pub fn builder() -> PrimitiveBuilder {
|
||||
PrimitiveBuilder::new(
|
||||
"Exposure",
|
||||
vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }],
|
||||
NodeSchema {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any }],
|
||||
output: vec![FieldSpec { name: "exposure", kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }],
|
||||
},
|
||||
|p| Box::new(Exposure::new(p[0].as_f64().expect("scale slot is F64"))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for Exposure {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: vec![InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }],
|
||||
output: vec![FieldSpec { name: "exposure", kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }],
|
||||
}
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1]
|
||||
}
|
||||
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -82,13 +83,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_params_match_built_node_schema() {
|
||||
let f = Exposure::factory();
|
||||
let built = f.build(&[Scalar::F64(0.5)]);
|
||||
assert_eq!(f.params(), built.schema().params.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exposure_is_none_until_input_present() {
|
||||
let mut e = Exposure::new(0.5);
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
//! indexed `F64` knobs that `Blueprint::param_space` aggregates (C8/C12/C19).
|
||||
|
||||
use aura_core::{
|
||||
Ctx, FieldSpec, Firing, InputSpec, LeafFactory, Node, NodeSchema, ParamSpec, Scalar, ScalarKind,
|
||||
Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar,
|
||||
ScalarKind,
|
||||
};
|
||||
|
||||
/// Weighted sum of `N` f64 inputs: `Σ weights[i] · input[i]`. The `weights` are
|
||||
@@ -39,16 +40,19 @@ impl LinComb {
|
||||
Self { weights, out: [Scalar::F64(0.0)] }
|
||||
}
|
||||
|
||||
/// The param-generic recipe for a blueprint leaf. The `arity` is topology
|
||||
/// (fixed per blueprint, C19), taken as a factory arg; only the weight *values*
|
||||
/// The param-generic recipe for a blueprint primitive. The `arity` is topology
|
||||
/// (fixed per blueprint, C19), taken as a builder arg; only the weight *values*
|
||||
/// are injected, slot by slot, through `LinComb::new` (the single sizing gate).
|
||||
pub fn factory(arity: usize) -> LeafFactory {
|
||||
pub fn builder(arity: usize) -> PrimitiveBuilder {
|
||||
let inputs = (0..arity)
|
||||
.map(|_| PortSpec { kind: ScalarKind::F64, firing: Firing::Any })
|
||||
.collect();
|
||||
let params = (0..arity)
|
||||
.map(|i| ParamSpec { name: format!("weights[{i}]"), kind: ScalarKind::F64 })
|
||||
.collect();
|
||||
LeafFactory::new(
|
||||
PrimitiveBuilder::new(
|
||||
"LinComb",
|
||||
params,
|
||||
NodeSchema { inputs, output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }], params },
|
||||
|p| Box::new(LinComb::new(
|
||||
p.iter().map(|s| s.as_f64().expect("weight slot is F64")).collect(),
|
||||
)),
|
||||
@@ -57,18 +61,8 @@ impl LinComb {
|
||||
}
|
||||
|
||||
impl Node for LinComb {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: self
|
||||
.weights
|
||||
.iter()
|
||||
.map(|_| InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any })
|
||||
.collect(),
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: (0..self.weights.len())
|
||||
.map(|i| ParamSpec { name: format!("weights[{i}]"), kind: ScalarKind::F64 })
|
||||
.collect(),
|
||||
}
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1; self.weights.len()]
|
||||
}
|
||||
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -142,13 +136,6 @@ mod tests {
|
||||
assert_eq!(lc.eval(Ctx::new(&inputs, Timestamp(0))), Some([Scalar::F64(6.0)].as_slice()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_params_match_built_node_schema() {
|
||||
let f = LinComb::factory(2);
|
||||
let built = f.build(&[Scalar::F64(1.0), Scalar::F64(-1.0)]);
|
||||
assert_eq!(f.params(), built.schema().params.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "LinComb needs at least one weight")]
|
||||
fn lincomb_empty_weights_panics() {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! four base scalar kinds so any column can be persisted; returns `None` (filters)
|
||||
//! until every input column is warm.
|
||||
|
||||
use aura_core::{Ctx, Firing, InputSpec, LeafFactory, Node, NodeSchema, Scalar, ScalarKind, Timestamp};
|
||||
use aura_core::{Ctx, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, Scalar, ScalarKind, Timestamp};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
/// A recording sink over `kinds.len()` input columns. Each fired cycle it reads
|
||||
@@ -16,42 +16,41 @@ use std::sync::mpsc::Sender;
|
||||
/// value.
|
||||
pub struct Recorder {
|
||||
kinds: Vec<ScalarKind>,
|
||||
firing: Firing,
|
||||
tx: Sender<(Timestamp, Vec<Scalar>)>,
|
||||
}
|
||||
|
||||
impl Recorder {
|
||||
/// A recorder over one input column per entry in `kinds`, each with the given
|
||||
/// `firing` policy, sending recorded `(timestamp, row)` pairs to `tx`.
|
||||
pub fn new(kinds: &[ScalarKind], firing: Firing, tx: Sender<(Timestamp, Vec<Scalar>)>) -> Self {
|
||||
Self { kinds: kinds.to_vec(), firing, tx }
|
||||
/// `firing` policy, sending recorded `(timestamp, row)` pairs to `tx`. The
|
||||
/// `firing` policy is a property of the declared signature (carried by the
|
||||
/// `PrimitiveBuilder`'s `PortSpec`s), not of the built sink, so it is part of
|
||||
/// the construction contract but not stored on the instance.
|
||||
pub fn new(kinds: &[ScalarKind], _firing: Firing, tx: Sender<(Timestamp, Vec<Scalar>)>) -> Self {
|
||||
Self { kinds: kinds.to_vec(), tx }
|
||||
}
|
||||
|
||||
/// The param-generic recipe for a blueprint leaf. The channel + kinds + firing
|
||||
/// The param-generic recipe for a blueprint primitive. The channel + kinds + firing
|
||||
/// are non-param construction args (captured), not tunable params; the node
|
||||
/// declares none. `tx` is cloned per build (`mpsc::Sender: Clone`).
|
||||
pub fn factory(
|
||||
/// declares none. The input ports (one per kind) are threaded into the schema
|
||||
/// statically; `tx`/`kinds` are cloned for the build closure.
|
||||
pub fn builder(
|
||||
kinds: Vec<ScalarKind>,
|
||||
firing: Firing,
|
||||
tx: Sender<(Timestamp, Vec<Scalar>)>,
|
||||
) -> LeafFactory {
|
||||
LeafFactory::new("Recorder", vec![], move |_| {
|
||||
Box::new(Recorder::new(&kinds, firing, tx.clone()))
|
||||
})
|
||||
) -> PrimitiveBuilder {
|
||||
let inputs = kinds.iter().map(|&kind| PortSpec { kind, firing }).collect();
|
||||
let build_kinds = kinds.clone();
|
||||
PrimitiveBuilder::new(
|
||||
"Recorder",
|
||||
NodeSchema { inputs, output: vec![], params: vec![] }, // sink: empty output (C8)
|
||||
move |_| Box::new(Recorder::new(&build_kinds, firing, tx.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for Recorder {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: self
|
||||
.kinds
|
||||
.iter()
|
||||
.map(|&kind| InputSpec { kind, lookback: 1, firing: self.firing })
|
||||
.collect(),
|
||||
output: vec![],
|
||||
params: vec![],
|
||||
}
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1; self.kinds.len()]
|
||||
}
|
||||
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -81,26 +80,22 @@ mod tests {
|
||||
use aura_core::{AnyColumn, Timestamp};
|
||||
use std::sync::mpsc;
|
||||
|
||||
#[test]
|
||||
fn factory_params_match_built_node_schema() {
|
||||
let (tx, _rx) = mpsc::channel();
|
||||
let f = Recorder::factory(vec![ScalarKind::F64], Firing::Any, tx);
|
||||
let built = f.build(&[]);
|
||||
assert_eq!(f.params(), built.schema().params.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recorder_captures_f64_stream_after_warmup() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut rec = Recorder::new(&[ScalarKind::F64], Firing::Any, tx);
|
||||
|
||||
// size the one f64 input column from the schema, as the engine would.
|
||||
let schema = rec.schema();
|
||||
assert!(schema.output.is_empty(), "a sink declares no output (C8)");
|
||||
let mut inputs = vec![AnyColumn::with_capacity(
|
||||
schema.inputs[0].kind,
|
||||
schema.inputs[0].lookback,
|
||||
)];
|
||||
// a sink declares no output (C8) — asserted on the param-generic builder
|
||||
let (tx_b, _rx_b) = mpsc::channel();
|
||||
assert!(
|
||||
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx_b)
|
||||
.schema()
|
||||
.output
|
||||
.is_empty(),
|
||||
"a sink declares no output (C8)"
|
||||
);
|
||||
// size the one f64 input column from the node's lookback, as bootstrap would.
|
||||
let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, rec.lookbacks()[0])];
|
||||
|
||||
// cold: returns None and records nothing.
|
||||
assert_eq!(rec.eval(Ctx::new(&inputs, Timestamp(1))), None);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! each cycle (decided at t-1) into a cumulative synthetic pip-equity output.
|
||||
//! Measures signal quality, not execution-modelled P&L.
|
||||
|
||||
use aura_core::{Ctx, FieldSpec, Firing, InputSpec, LeafFactory, Node, NodeSchema, Scalar, ScalarKind};
|
||||
use aura_core::{Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, Scalar, ScalarKind};
|
||||
|
||||
/// Integrates `exposure * price-return` into cumulative pips. `pip_size` is
|
||||
/// per-instrument reference metadata (beside the hot path, C7/C15), held here,
|
||||
@@ -55,24 +55,28 @@ impl SimBroker {
|
||||
}
|
||||
}
|
||||
|
||||
/// The param-generic recipe for a blueprint leaf. `pip_size` is per-instrument
|
||||
/// The param-generic recipe for a blueprint primitive. `pip_size` is per-instrument
|
||||
/// metadata (C10/C15), not a tunable param — it is captured by the closure, not
|
||||
/// injected; the node declares no params.
|
||||
pub fn factory(pip_size: f64) -> LeafFactory {
|
||||
LeafFactory::new("SimBroker", vec![], move |_| Box::new(SimBroker::new(pip_size)))
|
||||
pub fn builder(pip_size: f64) -> PrimitiveBuilder {
|
||||
PrimitiveBuilder::new(
|
||||
"SimBroker",
|
||||
NodeSchema {
|
||||
inputs: vec![
|
||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any }, // 0 exposure
|
||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any }, // 1 price
|
||||
],
|
||||
output: vec![FieldSpec { name: "equity", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
},
|
||||
move |_| Box::new(SimBroker::new(pip_size)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for SimBroker {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: vec![
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }, // 0 exposure
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any }, // 1 price
|
||||
],
|
||||
output: vec![FieldSpec { name: "equity", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1, 1]
|
||||
}
|
||||
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -122,13 +126,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_params_match_built_node_schema() {
|
||||
let f = SimBroker::factory(0.0001);
|
||||
let built = f.build(&[]);
|
||||
assert_eq!(f.params(), built.schema().params.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sim_broker_integrates_lagged_exposure_times_return() {
|
||||
let mut b = SimBroker::new(1.0);
|
||||
|
||||
+30
-37
@@ -4,7 +4,8 @@
|
||||
//! engine present (the test drives it by hand, as the sim loop later will).
|
||||
|
||||
use aura_core::{
|
||||
Ctx, FieldSpec, Firing, InputSpec, LeafFactory, Node, NodeSchema, ParamSpec, Scalar, ScalarKind,
|
||||
Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar,
|
||||
ScalarKind,
|
||||
};
|
||||
|
||||
/// Simple moving average over the last `length` values of one f64 input.
|
||||
@@ -20,29 +21,25 @@ impl Sma {
|
||||
Self { length, out: [Scalar::F64(0.0)] }
|
||||
}
|
||||
|
||||
/// The param-generic recipe for a blueprint leaf: declares `length` and builds
|
||||
/// The param-generic recipe for a blueprint primitive: declares `length` and builds
|
||||
/// through `Sma::new` (the single sizing/validation gate; the slice is
|
||||
/// kind-checked before `build` runs, so the typed read is total).
|
||||
pub fn factory() -> LeafFactory {
|
||||
LeafFactory::new(
|
||||
pub fn builder() -> PrimitiveBuilder {
|
||||
PrimitiveBuilder::new(
|
||||
"SMA",
|
||||
vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
NodeSchema {
|
||||
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any }],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
},
|
||||
|p| Box::new(Sma::new(p[0].as_i64().expect("length slot is I64") as usize)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Node for Sma {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: vec![InputSpec {
|
||||
kind: ScalarKind::F64,
|
||||
lookback: self.length,
|
||||
firing: Firing::Any,
|
||||
}],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
}
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![self.length]
|
||||
}
|
||||
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -70,14 +67,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn sma_warms_up_then_tracks_the_window_mean() {
|
||||
let mut sma = Sma::new(3);
|
||||
let schema = sma.schema();
|
||||
let sma_for_depth = Sma::new(3);
|
||||
|
||||
// size the input column from the schema, as the engine will at wiring
|
||||
let mut inputs = vec![AnyColumn::with_capacity(
|
||||
schema.inputs[0].kind,
|
||||
schema.inputs[0].lookback,
|
||||
)];
|
||||
// size the input column from the node's lookback, as bootstrap will at wiring
|
||||
let mut inputs =
|
||||
vec![AnyColumn::with_capacity(ScalarKind::F64, sma_for_depth.lookbacks()[0])];
|
||||
let mut sma = sma_for_depth;
|
||||
|
||||
let feed = [1.0_f64, 2.0, 3.0, 4.0, 5.0];
|
||||
// means of [1,2,3], [2,3,4], [3,4,5] once warmed up
|
||||
@@ -124,37 +119,35 @@ mod tests {
|
||||
assert_eq!(Recorder::new(&[ScalarKind::F64], Firing::Any, tx).label(), "Recorder");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_params_match_built_node_schema() {
|
||||
let f = Sma::factory();
|
||||
let built = f.build(&[Scalar::I64(3)]);
|
||||
assert_eq!(f.params(), built.schema().params.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nodes_declare_expected_params() {
|
||||
use crate::{Add, Exposure, LinComb, Recorder, SimBroker, Sub};
|
||||
use aura_core::{Firing, ParamSpec, ScalarKind};
|
||||
// single scalar knobs
|
||||
// single scalar knobs (declared on the param-generic builder, pre-build)
|
||||
assert_eq!(
|
||||
Sma::new(3).schema().params,
|
||||
Sma::builder().schema().params,
|
||||
vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
|
||||
);
|
||||
assert_eq!(
|
||||
Exposure::new(0.5).schema().params,
|
||||
Exposure::builder().schema().params,
|
||||
vec![ParamSpec { name: "scale".into(), kind: ScalarKind::F64 }],
|
||||
);
|
||||
// vector knob expands flat to N indexed F64 entries
|
||||
let lc = LinComb::new(vec![1.0, -1.0]).schema().params;
|
||||
let lc = LinComb::builder(2).schema().params.clone();
|
||||
assert_eq!(lc.len(), 2);
|
||||
assert_eq!(lc[0].name, "weights[0]");
|
||||
assert_eq!(lc[1].name, "weights[1]");
|
||||
assert!(lc.iter().all(|p| p.kind == ScalarKind::F64));
|
||||
// param-less nodes declare empty
|
||||
assert!(Sub::new().schema().params.is_empty());
|
||||
assert!(Add::new().schema().params.is_empty());
|
||||
assert!(SimBroker::new(0.0001).schema().params.is_empty());
|
||||
assert!(Sub::builder().schema().params.is_empty());
|
||||
assert!(Add::builder().schema().params.is_empty());
|
||||
assert!(SimBroker::builder(0.0001).schema().params.is_empty());
|
||||
let (tx, _rx) = std::sync::mpsc::channel();
|
||||
assert!(Recorder::new(&[ScalarKind::F64], Firing::Any, tx).schema().params.is_empty());
|
||||
assert!(
|
||||
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx)
|
||||
.schema()
|
||||
.params
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-20
@@ -3,7 +3,7 @@
|
||||
//! real fan-out + join to run (two SMAs joining into one node), exercising
|
||||
//! multi-input `Ctx` access inside a running graph.
|
||||
|
||||
use aura_core::{Ctx, FieldSpec, Firing, InputSpec, LeafFactory, Node, NodeSchema, Scalar, ScalarKind};
|
||||
use aura_core::{Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, Scalar, ScalarKind};
|
||||
|
||||
/// Two-input f64 difference: input 0 minus input 1. Emits `None` until both
|
||||
/// inputs have a value.
|
||||
@@ -17,10 +17,21 @@ impl Sub {
|
||||
Self { out: [Scalar::F64(0.0)] }
|
||||
}
|
||||
|
||||
/// The param-generic recipe for a blueprint leaf: paramless, builds through
|
||||
/// The param-generic recipe for a blueprint primitive: paramless, builds through
|
||||
/// `Sub::new`.
|
||||
pub fn factory() -> LeafFactory {
|
||||
LeafFactory::new("Sub", vec![], |_| Box::new(Sub::new()))
|
||||
pub fn builder() -> PrimitiveBuilder {
|
||||
PrimitiveBuilder::new(
|
||||
"Sub",
|
||||
NodeSchema {
|
||||
inputs: vec![
|
||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any },
|
||||
PortSpec { kind: ScalarKind::F64, firing: Firing::Any },
|
||||
],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
},
|
||||
|_| Box::new(Sub::new()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,15 +42,8 @@ impl Default for Sub {
|
||||
}
|
||||
|
||||
impl Node for Sub {
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
inputs: vec![
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any },
|
||||
InputSpec { kind: ScalarKind::F64, lookback: 1, firing: Firing::Any },
|
||||
],
|
||||
output: vec![FieldSpec { name: "value", kind: ScalarKind::F64 }],
|
||||
params: vec![],
|
||||
}
|
||||
fn lookbacks(&self) -> Vec<usize> {
|
||||
vec![1, 1]
|
||||
}
|
||||
|
||||
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
||||
@@ -62,13 +66,6 @@ mod tests {
|
||||
use super::*;
|
||||
use aura_core::{AnyColumn, Timestamp};
|
||||
|
||||
#[test]
|
||||
fn factory_params_match_built_node_schema() {
|
||||
let f = Sub::factory();
|
||||
let built = f.build(&[]);
|
||||
assert_eq!(f.params(), built.schema().params.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_is_difference_once_both_inputs_present() {
|
||||
let mut sub = Sub::new();
|
||||
|
||||
Reference in New Issue
Block a user