feat(std): fill the by-chance vocabulary gaps — Const, Div, Abs, Max, Min
Five new rostered node types (count-pin 23 -> 28, both the in-crate shape test and the cross-boundary CLI vocabulary e2e): Const is unary with an f64 'value' param — the clock input drives it, its value is ignored, since a zero-input node never evaluates in the total-push engine — mirroring EqConst's constant-as-param pattern; Div is binary IEEE-754 (x/0 -> signed inf, 0/0 -> NaN, unit-tested, no error channel); Abs unary mirroring Sqrt; Max/Min binary pairwise, distinct from the windowed RollingMax/RollingMin. Acceptance proof: the committed executable spec composes an RSI-class gain/loss-split-and-ratio signal purely from blueprint data through std_vocabulary and runs it to hand-computed RS values — the r_meanrev constant-folding workaround is no longer forced. Verified: headline test green, aura-std 163/0, full workspace suite green (independent mini-verify), clippy -D warnings clean. closes #236
This commit is contained in:
@@ -121,8 +121,8 @@ fn graph_introspect_vocabulary_lists_exactly_the_closed_roster_count() {
|
|||||||
let lines: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect();
|
let lines: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
lines.len(),
|
lines.len(),
|
||||||
23,
|
28,
|
||||||
"the std-only (no project) vocabulary has exactly the roster's 23 entries: {stdout}"
|
"the std-only (no project) vocabulary has exactly the roster's 28 entries: {stdout}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
//! `Abs` — one-input f64 absolute value. Turns a signed delta (e.g. the
|
||||||
|
//! negative-side split of a price change) into a positive magnitude, e.g.
|
||||||
|
//! RSI-class signals' `loss = Abs(Min(delta, 0))`. Emits `None` until its
|
||||||
|
//! input has a value.
|
||||||
|
|
||||||
|
use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind};
|
||||||
|
|
||||||
|
/// One-input f64 absolute value, `value.abs()`. Emits `None` until its input
|
||||||
|
/// has a value (warm-up filter, C8).
|
||||||
|
pub struct Abs {
|
||||||
|
out: [Cell; 1],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Abs {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { out: [Cell::from_f64(0.0)] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn builder() -> PrimitiveBuilder {
|
||||||
|
PrimitiveBuilder::new(
|
||||||
|
"Abs",
|
||||||
|
NodeSchema {
|
||||||
|
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "value".into() }],
|
||||||
|
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||||
|
params: vec![],
|
||||||
|
},
|
||||||
|
|_| Box::new(Abs::new()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Abs {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node for Abs {
|
||||||
|
fn lookbacks(&self) -> Vec<usize> {
|
||||||
|
vec![1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
|
||||||
|
let w = ctx.f64_in(0);
|
||||||
|
if w.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.out[0] = Cell::from_f64(w[0].abs());
|
||||||
|
Some(&self.out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> String {
|
||||||
|
"Abs".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use aura_core::{AnyColumn, Scalar, Timestamp};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn abs_of_negative_and_positive_is_the_magnitude() {
|
||||||
|
let mut a = Abs::new();
|
||||||
|
let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
|
||||||
|
inputs[0].push(Scalar::f64(-3.0)).unwrap();
|
||||||
|
assert_eq!(a.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(3.0)].as_slice()));
|
||||||
|
inputs[0].push(Scalar::f64(3.0)).unwrap();
|
||||||
|
assert_eq!(a.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(3.0)].as_slice()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn abs_is_none_until_input_present() {
|
||||||
|
let mut a = Abs::new();
|
||||||
|
let inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
|
||||||
|
assert_eq!(a.eval(Ctx::new(&inputs, Timestamp(0))), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
//! `Const` — a constant-as-param source: emits a fixed `f64` value every cycle
|
||||||
|
//! its clock input fires, ignoring the clock's actual value. Mirrors `EqConst`'s
|
||||||
|
//! constant-as-param pattern (the constant is a bound param, not topology), but
|
||||||
|
//! as a *source*: a zero-input node never evaluates in the total-push engine
|
||||||
|
//! (C8), so `Const` still needs one input purely to be driven by the sim clock
|
||||||
|
//! — its value is discarded.
|
||||||
|
|
||||||
|
use aura_core::{
|
||||||
|
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
|
||||||
|
ScalarKind,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Emits the bound `value` every cycle its clock input fires; the clock's own
|
||||||
|
/// value is ignored. Emits `None` until the clock input has fired at least once
|
||||||
|
/// (warm-up filter, C8) — same shape as every other stateless node here.
|
||||||
|
pub struct Const {
|
||||||
|
value: f64,
|
||||||
|
out: [Cell; 1],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Const {
|
||||||
|
/// Build a constant source bound to `value`.
|
||||||
|
pub fn new(value: f64) -> Self {
|
||||||
|
Self { value, out: [Cell::from_f64(value)] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The param-generic recipe for a blueprint primitive: declares `value` and
|
||||||
|
/// one clock input, builds through `Const::new`.
|
||||||
|
pub fn builder() -> PrimitiveBuilder {
|
||||||
|
PrimitiveBuilder::new(
|
||||||
|
"Const",
|
||||||
|
NodeSchema {
|
||||||
|
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "clock".into() }],
|
||||||
|
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||||
|
params: vec![ParamSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||||
|
},
|
||||||
|
|p| Box::new(Const::new(p[0].f64())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node for Const {
|
||||||
|
fn lookbacks(&self) -> Vec<usize> {
|
||||||
|
vec![1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
|
||||||
|
let w = ctx.f64_in(0);
|
||||||
|
if w.is_empty() {
|
||||||
|
return None; // not yet warmed up (C8 filter)
|
||||||
|
}
|
||||||
|
self.out[0] = Cell::from_f64(self.value);
|
||||||
|
Some(&self.out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> String {
|
||||||
|
format!("Const({})", self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use aura_core::{AnyColumn, Scalar, Timestamp};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn const_emits_bound_value_regardless_of_clock_value() {
|
||||||
|
// The core property: out == value, always, once the clock has fired at
|
||||||
|
// least once — the clock's own reading is never observed in the output.
|
||||||
|
let mut node = Const::new(7.0);
|
||||||
|
let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
|
||||||
|
|
||||||
|
inputs[0].push(Scalar::f64(-100.0)).unwrap();
|
||||||
|
assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(7.0)].as_slice()));
|
||||||
|
|
||||||
|
inputs[0].push(Scalar::f64(42.0)).unwrap();
|
||||||
|
assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(7.0)].as_slice()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn const_is_none_until_clock_present() {
|
||||||
|
let mut node = Const::new(7.0);
|
||||||
|
let inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
|
||||||
|
assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
//! `Div` — two-input f64 quotient (input 0 divided by input 1), the ratio
|
||||||
|
//! combinator RSI-class signals need (`avg_gain / avg_loss`). IEEE-754
|
||||||
|
//! division, no error channel: `x / 0.0 -> inf` (signed by `x`'s sign),
|
||||||
|
//! `0.0 / 0.0 -> NaN` — Rust's native `f64` division already follows this,
|
||||||
|
//! so no special-casing is needed.
|
||||||
|
|
||||||
|
use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind};
|
||||||
|
|
||||||
|
/// Two-input f64 quotient: input 0 divided by input 1 (IEEE-754). Emits `None`
|
||||||
|
/// until both inputs have a value.
|
||||||
|
pub struct Div {
|
||||||
|
out: [Cell; 1],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Div {
|
||||||
|
/// Build a `Div` node.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { out: [Cell::from_f64(0.0)] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The param-generic recipe for a blueprint primitive: paramless, builds
|
||||||
|
/// through `Div::new`.
|
||||||
|
pub fn builder() -> PrimitiveBuilder {
|
||||||
|
PrimitiveBuilder::new(
|
||||||
|
"Div",
|
||||||
|
NodeSchema {
|
||||||
|
inputs: vec![
|
||||||
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() },
|
||||||
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "rhs".into() },
|
||||||
|
],
|
||||||
|
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||||
|
params: vec![],
|
||||||
|
},
|
||||||
|
|_| Box::new(Div::new()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Div {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node for Div {
|
||||||
|
fn lookbacks(&self) -> Vec<usize> {
|
||||||
|
vec![1, 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
|
||||||
|
let a = ctx.f64_in(0);
|
||||||
|
let b = ctx.f64_in(1);
|
||||||
|
if a.is_empty() || b.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.out[0] = Cell::from_f64(a[0] / b[0]);
|
||||||
|
Some(&self.out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> String {
|
||||||
|
"Div".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use aura_core::{AnyColumn, Scalar, Timestamp};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn div_is_quotient_once_both_inputs_present() {
|
||||||
|
let mut div = Div::new();
|
||||||
|
let mut inputs = vec![
|
||||||
|
AnyColumn::with_capacity(ScalarKind::F64, 1),
|
||||||
|
AnyColumn::with_capacity(ScalarKind::F64, 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
// only input 0 present -> None
|
||||||
|
inputs[0].push(Scalar::f64(10.0)).unwrap();
|
||||||
|
assert_eq!(div.eval(Ctx::new(&inputs, Timestamp(0))), None);
|
||||||
|
|
||||||
|
// both present -> a / b
|
||||||
|
inputs[1].push(Scalar::f64(4.0)).unwrap();
|
||||||
|
assert_eq!(div.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(2.5)].as_slice()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn div_by_zero_is_signed_infinity_zero_over_zero_is_nan() {
|
||||||
|
// The IEEE-754 property that makes Div safe with no error channel: a
|
||||||
|
// nonzero numerator over zero yields a signed infinity, and 0/0 yields
|
||||||
|
// NaN — both representable as plain f64, never a panic or an Err.
|
||||||
|
let mut div = Div::new();
|
||||||
|
let mut inputs = vec![
|
||||||
|
AnyColumn::with_capacity(ScalarKind::F64, 1),
|
||||||
|
AnyColumn::with_capacity(ScalarKind::F64, 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
inputs[0].push(Scalar::f64(5.0)).unwrap();
|
||||||
|
inputs[1].push(Scalar::f64(0.0)).unwrap();
|
||||||
|
let got = div.eval(Ctx::new(&inputs, Timestamp(0))).unwrap();
|
||||||
|
assert_eq!(got[0].f64(), f64::INFINITY);
|
||||||
|
|
||||||
|
inputs[0].push(Scalar::f64(0.0)).unwrap();
|
||||||
|
inputs[1].push(Scalar::f64(0.0)).unwrap();
|
||||||
|
let got = div.eval(Ctx::new(&inputs, Timestamp(0))).unwrap();
|
||||||
|
assert!(got[0].f64().is_nan());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn input_slots_are_named_lhs_rhs() {
|
||||||
|
let names: Vec<String> = Div::builder().schema().inputs.iter().map(|p| p.name.clone()).collect();
|
||||||
|
assert_eq!(names, ["lhs", "rhs"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,14 +17,17 @@
|
|||||||
//! The first block lands with the walking skeleton: [`Sma`], the simple moving
|
//! The first block lands with the walking skeleton: [`Sma`], the simple moving
|
||||||
//! average — a worked producer node proving the `aura-core` `Node` contract.
|
//! average — a worked producer node proving the `aura-core` `Node` contract.
|
||||||
|
|
||||||
|
mod abs;
|
||||||
mod add;
|
mod add;
|
||||||
mod and;
|
mod and;
|
||||||
mod bias;
|
mod bias;
|
||||||
mod carry_cost;
|
mod carry_cost;
|
||||||
|
mod const_node;
|
||||||
mod constant_cost;
|
mod constant_cost;
|
||||||
mod cost;
|
mod cost;
|
||||||
mod cost_sum;
|
mod cost_sum;
|
||||||
mod delay;
|
mod delay;
|
||||||
|
mod div;
|
||||||
mod ema;
|
mod ema;
|
||||||
mod eqconst;
|
mod eqconst;
|
||||||
mod gated_recorder;
|
mod gated_recorder;
|
||||||
@@ -32,6 +35,8 @@ mod gt;
|
|||||||
mod latch;
|
mod latch;
|
||||||
mod lincomb;
|
mod lincomb;
|
||||||
mod longonly;
|
mod longonly;
|
||||||
|
mod max;
|
||||||
|
mod min;
|
||||||
mod mul;
|
mod mul;
|
||||||
mod position_management;
|
mod position_management;
|
||||||
mod recorder;
|
mod recorder;
|
||||||
@@ -49,10 +54,12 @@ mod stop_rule;
|
|||||||
mod sub;
|
mod sub;
|
||||||
mod vocabulary;
|
mod vocabulary;
|
||||||
mod vol_slippage_cost;
|
mod vol_slippage_cost;
|
||||||
|
pub use abs::Abs;
|
||||||
pub use add::Add;
|
pub use add::Add;
|
||||||
pub use and::And;
|
pub use and::And;
|
||||||
pub use bias::Bias;
|
pub use bias::Bias;
|
||||||
pub use carry_cost::CarryCost;
|
pub use carry_cost::CarryCost;
|
||||||
|
pub use const_node::Const;
|
||||||
pub use constant_cost::ConstantCost;
|
pub use constant_cost::ConstantCost;
|
||||||
pub use cost::{
|
pub use cost::{
|
||||||
cost_node_builder, ChargeMode, CostNode, CostRunner, COST_FIELD_NAMES, COST_WIDTH,
|
cost_node_builder, ChargeMode, CostNode, CostRunner, COST_FIELD_NAMES, COST_WIDTH,
|
||||||
@@ -60,6 +67,7 @@ pub use cost::{
|
|||||||
};
|
};
|
||||||
pub use cost_sum::CostSum;
|
pub use cost_sum::CostSum;
|
||||||
pub use delay::Delay;
|
pub use delay::Delay;
|
||||||
|
pub use div::Div;
|
||||||
pub use ema::Ema;
|
pub use ema::Ema;
|
||||||
pub use eqconst::EqConst;
|
pub use eqconst::EqConst;
|
||||||
pub use gated_recorder::GatedRecorder;
|
pub use gated_recorder::GatedRecorder;
|
||||||
@@ -67,6 +75,8 @@ pub use gt::Gt;
|
|||||||
pub use latch::Latch;
|
pub use latch::Latch;
|
||||||
pub use lincomb::LinComb;
|
pub use lincomb::LinComb;
|
||||||
pub use longonly::LongOnly;
|
pub use longonly::LongOnly;
|
||||||
|
pub use max::Max;
|
||||||
|
pub use min::Min;
|
||||||
pub use mul::Mul;
|
pub use mul::Mul;
|
||||||
pub use position_management::{
|
pub use position_management::{
|
||||||
ExitReason, FIELD_NAMES as PM_FIELD_NAMES, PositionManagement, RECORD_KINDS as PM_RECORD_KINDS,
|
ExitReason, FIELD_NAMES as PM_FIELD_NAMES, PositionManagement, RECORD_KINDS as PM_RECORD_KINDS,
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
//! `Max` — two-input f64 pairwise maximum (input 0 vs input 1), e.g. the
|
||||||
|
//! positive-part split of a price change: `gain = Max(delta, 0)`. Distinct
|
||||||
|
//! from `RollingMax`, which reduces a *window* of one input; `Max` combines
|
||||||
|
//! two co-present inputs, mirroring `Add`/`Sub`.
|
||||||
|
|
||||||
|
use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind};
|
||||||
|
|
||||||
|
/// Two-input f64 pairwise maximum: `input 0 .max(input 1)`. Emits `None` until
|
||||||
|
/// both inputs have a value.
|
||||||
|
pub struct Max {
|
||||||
|
out: [Cell; 1],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Max {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { out: [Cell::from_f64(0.0)] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn builder() -> PrimitiveBuilder {
|
||||||
|
PrimitiveBuilder::new(
|
||||||
|
"Max",
|
||||||
|
NodeSchema {
|
||||||
|
inputs: vec![
|
||||||
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() },
|
||||||
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "rhs".into() },
|
||||||
|
],
|
||||||
|
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||||
|
params: vec![],
|
||||||
|
},
|
||||||
|
|_| Box::new(Max::new()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Max {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node for Max {
|
||||||
|
fn lookbacks(&self) -> Vec<usize> {
|
||||||
|
vec![1, 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
|
||||||
|
let a = ctx.f64_in(0);
|
||||||
|
let b = ctx.f64_in(1);
|
||||||
|
if a.is_empty() || b.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.out[0] = Cell::from_f64(a[0].max(b[0]));
|
||||||
|
Some(&self.out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> String {
|
||||||
|
"Max".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use aura_core::{AnyColumn, Scalar, Timestamp};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn max_is_the_pairwise_larger_once_both_inputs_present() {
|
||||||
|
let mut m = Max::new();
|
||||||
|
let mut inputs = vec![
|
||||||
|
AnyColumn::with_capacity(ScalarKind::F64, 1),
|
||||||
|
AnyColumn::with_capacity(ScalarKind::F64, 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
// only input 0 present -> None
|
||||||
|
inputs[0].push(Scalar::f64(2.0)).unwrap();
|
||||||
|
assert_eq!(m.eval(Ctx::new(&inputs, Timestamp(0))), None);
|
||||||
|
|
||||||
|
// both present -> max(a, b), either order
|
||||||
|
inputs[1].push(Scalar::f64(-1.0)).unwrap();
|
||||||
|
assert_eq!(m.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(2.0)].as_slice()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn input_slots_are_named_lhs_rhs() {
|
||||||
|
let names: Vec<String> = Max::builder().schema().inputs.iter().map(|p| p.name.clone()).collect();
|
||||||
|
assert_eq!(names, ["lhs", "rhs"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
//! `Min` — two-input f64 pairwise minimum (input 0 vs input 1), the companion
|
||||||
|
//! to `Max`, e.g. the negative-part split of a price change:
|
||||||
|
//! `loss = Abs(Min(delta, 0))`. Distinct from `RollingMin`, which reduces a
|
||||||
|
//! *window* of one input; `Min` combines two co-present inputs, mirroring
|
||||||
|
//! `Add`/`Sub`.
|
||||||
|
|
||||||
|
use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind};
|
||||||
|
|
||||||
|
/// Two-input f64 pairwise minimum: `input 0 .min(input 1)`. Emits `None` until
|
||||||
|
/// both inputs have a value.
|
||||||
|
pub struct Min {
|
||||||
|
out: [Cell; 1],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Min {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { out: [Cell::from_f64(0.0)] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn builder() -> PrimitiveBuilder {
|
||||||
|
PrimitiveBuilder::new(
|
||||||
|
"Min",
|
||||||
|
NodeSchema {
|
||||||
|
inputs: vec![
|
||||||
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "lhs".into() },
|
||||||
|
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "rhs".into() },
|
||||||
|
],
|
||||||
|
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
|
||||||
|
params: vec![],
|
||||||
|
},
|
||||||
|
|_| Box::new(Min::new()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Min {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node for Min {
|
||||||
|
fn lookbacks(&self) -> Vec<usize> {
|
||||||
|
vec![1, 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
|
||||||
|
let a = ctx.f64_in(0);
|
||||||
|
let b = ctx.f64_in(1);
|
||||||
|
if a.is_empty() || b.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.out[0] = Cell::from_f64(a[0].min(b[0]));
|
||||||
|
Some(&self.out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> String {
|
||||||
|
"Min".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use aura_core::{AnyColumn, Scalar, Timestamp};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn min_is_the_pairwise_smaller_once_both_inputs_present() {
|
||||||
|
let mut m = Min::new();
|
||||||
|
let mut inputs = vec![
|
||||||
|
AnyColumn::with_capacity(ScalarKind::F64, 1),
|
||||||
|
AnyColumn::with_capacity(ScalarKind::F64, 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
// only input 0 present -> None
|
||||||
|
inputs[0].push(Scalar::f64(2.0)).unwrap();
|
||||||
|
assert_eq!(m.eval(Ctx::new(&inputs, Timestamp(0))), None);
|
||||||
|
|
||||||
|
// both present -> min(a, b), either order
|
||||||
|
inputs[1].push(Scalar::f64(-1.0)).unwrap();
|
||||||
|
assert_eq!(m.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(-1.0)].as_slice()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn input_slots_are_named_lhs_rhs() {
|
||||||
|
let names: Vec<String> = Min::builder().schema().inputs.iter().map(|p| p.name.clone()).collect();
|
||||||
|
assert_eq!(names, ["lhs", "rhs"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,9 +18,9 @@
|
|||||||
//! later, additive extension (#156/C20).
|
//! later, additive extension (#156/C20).
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Add, And, Bias, CarryCost, ConstantCost, Delay, Ema, EqConst, FixedStop, Gt, Latch, LongOnly,
|
Abs, Add, And, Bias, CarryCost, Const, ConstantCost, Delay, Div, Ema, EqConst, FixedStop, Gt,
|
||||||
Mul, PositionManagement, Resample, RollingMax, RollingMin, Scale, Sizer, Sma, Sqrt, Sub,
|
Latch, LongOnly, Max, Min, Mul, PositionManagement, Resample, RollingMax, RollingMin, Scale,
|
||||||
VolSlippageCost,
|
Sizer, Sma, Sqrt, Sub, VolSlippageCost,
|
||||||
};
|
};
|
||||||
use aura_core::PrimitiveBuilder;
|
use aura_core::PrimitiveBuilder;
|
||||||
|
|
||||||
@@ -58,18 +58,23 @@ macro_rules! std_vocabulary_roster {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std_vocabulary_roster! {
|
std_vocabulary_roster! {
|
||||||
|
"Abs" => Abs,
|
||||||
"Add" => Add,
|
"Add" => Add,
|
||||||
"And" => And,
|
"And" => And,
|
||||||
"Bias" => Bias,
|
"Bias" => Bias,
|
||||||
"CarryCost" => CarryCost,
|
"CarryCost" => CarryCost,
|
||||||
|
"Const" => Const,
|
||||||
"ConstantCost" => ConstantCost,
|
"ConstantCost" => ConstantCost,
|
||||||
"Delay" => Delay,
|
"Delay" => Delay,
|
||||||
|
"Div" => Div,
|
||||||
"EMA" => Ema,
|
"EMA" => Ema,
|
||||||
"EqConst" => EqConst,
|
"EqConst" => EqConst,
|
||||||
"FixedStop" => FixedStop,
|
"FixedStop" => FixedStop,
|
||||||
"Gt" => Gt,
|
"Gt" => Gt,
|
||||||
"Latch" => Latch,
|
"Latch" => Latch,
|
||||||
"LongOnly" => LongOnly,
|
"LongOnly" => LongOnly,
|
||||||
|
"Max" => Max,
|
||||||
|
"Min" => Min,
|
||||||
"Mul" => Mul,
|
"Mul" => Mul,
|
||||||
"PositionManagement" => PositionManagement,
|
"PositionManagement" => PositionManagement,
|
||||||
"Resample" => Resample,
|
"Resample" => Resample,
|
||||||
@@ -124,7 +129,7 @@ mod tests {
|
|||||||
assert!(!std_vocabulary_types().contains(&"LinComb")); // construction-arg node
|
assert!(!std_vocabulary_types().contains(&"LinComb")); // construction-arg node
|
||||||
assert!(!std_vocabulary_types().contains(&"Recorder")); // sink
|
assert!(!std_vocabulary_types().contains(&"Recorder")); // sink
|
||||||
assert!(!std_vocabulary_types().contains(&"nope"));
|
assert!(!std_vocabulary_types().contains(&"nope"));
|
||||||
// count guard: pins the roster at exactly 23 entries
|
// count guard: pins the roster at exactly 28 entries
|
||||||
assert_eq!(std_vocabulary_types().len(), 23);
|
assert_eq!(std_vocabulary_types().len(), 28);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user