Files
Aura/crates/aura-std/src/gt.rs
T
claude a32dc38d18 feat(std): meaning lines for all 27 shipped node schemas
C29 compile/unit seam, task 3 of the self-description plan: every
aura-std NodeSchema literal threads its one-line doc. Four texts were
corrected against the actual eval/finalize semantics rather than taken
from the plan table verbatim: Delay is a lag-N register (not one-step),
GatedRecorder flushes an ungated final row at finalize, Latch is a
level-sensitive set/reset register (captures no input), SeriesReducer
emits its single summary row at finalize (not per cycle).

Gate: cargo build -p aura-std --lib clean; full std test run follows at
the all-crates gate once strategy/backtest/engine thread their sites
(the earlier per-crate test gate was unsatisfiable in isolation --
std's dev-deps pull crates whose sites belong to later tasks).

refs #316
2026-07-23 15:25:16 +02:00

129 lines
4.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `Gt` — a stateless `f64 × f64 -> bool` comparator: `out = (a > b)`, STRICT
//! greater-than.
//!
//! The first **bool-emitting f64 comparator** in `aura-std` (the `f64` cousin of
//! `EqConst`'s `i64 -> bool` gate). Its purpose in the session-breakout strategy
//! is `breakout = close15 > prevHigh15`: a fresh 15m close strictly above the
//! prior bar's high. **Strict `>` is load-bearing** — a close exactly *equal* to
//! the previous high is **not** a breakout, so `a == b` emits `false`.
//!
//! The **operator is topology** — `Gt` is a concrete node type, not an op
//! selected by a swept param. The relational siblings (`Lt`, `Ge`, `Le`, `Eq`)
//! would each be their own node type, never a param. Two `f64` inputs
//! (`Firing::Any`), one `bool` output, allocation-free on the hot path (the
//! single-cell output buffer is sized once at construction, C7).
use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind};
/// Stateless `f64 × f64 -> bool` strict comparator: emits `a > b` each cycle.
/// Emits `None` until **both** inputs have a value (warm-up gate, C8).
pub struct Gt {
out: [Cell; 1],
}
impl Gt {
/// Build a `Gt` node.
pub fn new() -> Self {
Self { out: [Cell::from_bool(false)] }
}
/// The param-generic recipe for a blueprint primitive: paramless, builds
/// through `Gt::new`.
pub fn builder() -> PrimitiveBuilder {
PrimitiveBuilder::new(
"Gt",
NodeSchema {
inputs: vec![
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "a".into() },
PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "b".into() },
],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::Bool }],
params: vec![],
doc: "true where the first input exceeds the second",
},
|_| Box::new(Gt::new()),
)
}
}
impl Default for Gt {
fn default() -> Self {
Self::new()
}
}
impl Node for Gt {
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; // not yet warmed up — both legs required (C8 filter)
}
self.out[0] = Cell::from_bool(a[0] > b[0]); // STRICT: a == b -> false
Some(&self.out)
}
fn label(&self) -> String {
"Gt".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use aura_core::{AnyColumn, Scalar, Timestamp};
#[test]
fn gt_is_strict_greater_than_once_both_inputs_present() {
// The core property: out == (a > b), STRICT. Three regions are covered,
// and the a==b case is the load-bearing one — a close exactly equal to
// the previous high is NOT a breakout, so equality emits `false`.
let mut node = Gt::new();
let mut inputs = vec![
AnyColumn::with_capacity(ScalarKind::F64, 1),
AnyColumn::with_capacity(ScalarKind::F64, 1),
];
let a_feed = [1.0_f64, 5.0, 3.0];
let b_feed = [2.0_f64, 2.0, 3.0];
// a<b a>b a==b
let expect = [Some(false), Some(true), Some(false)];
for ((av, bv), want) in a_feed.iter().zip(b_feed.iter()).zip(expect) {
inputs[0].push(Scalar::f64(*av)).unwrap();
inputs[1].push(Scalar::f64(*bv)).unwrap();
let got = node.eval(Ctx::new(&inputs, Timestamp(0)));
assert_eq!(got, Some([Cell::from_bool(want.unwrap())].as_slice()));
}
}
#[test]
fn gt_is_none_until_both_inputs_present() {
// Both-inputs warm-up gate (C8), like `Sub`: only one leg present -> None.
let mut node = Gt::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!(node.eval(Ctx::new(&inputs, Timestamp(0))), None);
// both present -> a strict-gt bool (10.0 > 4.0 == true)
inputs[1].push(Scalar::f64(4.0)).unwrap();
assert_eq!(node.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_bool(true)].as_slice()));
}
#[test]
fn input_slots_are_named_a_b() {
let g = Gt::builder();
let names: Vec<&str> = g.schema().inputs.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, ["a", "b"]);
}
}