feat(aura-std): add Mul + Sqrt primitives
The two genuinely-missing arithmetic primitives (Mul = two-stream f64 product, Sqrt = one-input f64 root, negatives clamped to 0). They are the building blocks for the volatility stop as a composition (rolling EWMA stddev), replacing the fused VolStop node. Also corrects plan 0066 Task 3 (the VolStop removal must migrate its stage1_r_e2e.rs caller — a false premise the implementer caught). refs #117 #119
This commit is contained in:
@@ -25,12 +25,14 @@ mod gt;
|
||||
mod latch;
|
||||
mod lincomb;
|
||||
mod longonly;
|
||||
mod mul;
|
||||
mod position_management;
|
||||
mod recorder;
|
||||
mod resample;
|
||||
mod session;
|
||||
mod sim_broker;
|
||||
mod sma;
|
||||
mod sqrt;
|
||||
mod stop_rule;
|
||||
mod sub;
|
||||
pub use add::Add;
|
||||
@@ -43,6 +45,7 @@ pub use gt::Gt;
|
||||
pub use latch::Latch;
|
||||
pub use lincomb::LinComb;
|
||||
pub use longonly::LongOnly;
|
||||
pub use mul::Mul;
|
||||
pub use position_management::{
|
||||
ExitReason, FIELD_NAMES as PM_FIELD_NAMES, PositionManagement, RECORD_KINDS as PM_RECORD_KINDS,
|
||||
WIDTH as PM_WIDTH,
|
||||
@@ -52,5 +55,6 @@ pub use resample::Resample;
|
||||
pub use session::Session;
|
||||
pub use sim_broker::SimBroker;
|
||||
pub use sma::Sma;
|
||||
pub use sqrt::Sqrt;
|
||||
pub use stop_rule::{FixedStop, VolStop};
|
||||
pub use sub::Sub;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
//! `Mul` — two-input f64 product (input 0 times input 1). The fundamental
|
||||
//! multiplication primitive (e.g. squaring a return for a variance estimate:
|
||||
//! `Mul(delta, delta)`). Emits `None` until both inputs have a value.
|
||||
|
||||
use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind};
|
||||
|
||||
/// Two-input f64 product: input 0 times input 1. Emits `None` until both inputs
|
||||
/// have a value.
|
||||
pub struct Mul {
|
||||
out: [Cell; 1],
|
||||
}
|
||||
|
||||
impl Mul {
|
||||
pub fn new() -> Self {
|
||||
Self { out: [Cell::from_f64(0.0)] }
|
||||
}
|
||||
pub fn builder() -> PrimitiveBuilder {
|
||||
PrimitiveBuilder::new(
|
||||
"Mul",
|
||||
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(Mul::new()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Mul {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
|
||||
impl Node for Mul {
|
||||
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 { "Mul".to_string() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use aura_core::{AnyColumn, Scalar, Timestamp};
|
||||
|
||||
#[test]
|
||||
fn mul_is_product_once_both_inputs_present() {
|
||||
let mut m = Mul::new();
|
||||
let mut inputs = vec![
|
||||
AnyColumn::with_capacity(ScalarKind::F64, 1),
|
||||
AnyColumn::with_capacity(ScalarKind::F64, 1),
|
||||
];
|
||||
inputs[0].push(Scalar::f64(3.0)).unwrap();
|
||||
assert_eq!(m.eval(Ctx::new(&inputs, Timestamp(0))), None); // only one leg
|
||||
inputs[1].push(Scalar::f64(4.0)).unwrap();
|
||||
assert_eq!(m.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(12.0)].as_slice()));
|
||||
}
|
||||
#[test]
|
||||
fn input_slots_are_named_lhs_rhs() {
|
||||
let names: Vec<String> = Mul::builder().schema().inputs.iter().map(|p| p.name.clone()).collect();
|
||||
assert_eq!(names, ["lhs", "rhs"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//! `Sqrt` — one-input f64 square root. Turns a variance estimate (price²) back
|
||||
//! into a standard deviation (price), e.g. the last stage of a rolling-stddev
|
||||
//! volatility. Negative inputs are clamped to `0.0` before the root (a variance
|
||||
//! is non-negative; floating rounding can produce a tiny negative). Emits `None`
|
||||
//! until its input has a value.
|
||||
|
||||
use aura_core::{Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, PortSpec, PrimitiveBuilder, ScalarKind};
|
||||
|
||||
/// One-input f64 square root, `sqrt(max(input, 0.0))`. Emits `None` until its
|
||||
/// input has a value (warm-up filter, C8).
|
||||
pub struct Sqrt {
|
||||
out: [Cell; 1],
|
||||
}
|
||||
|
||||
impl Sqrt {
|
||||
pub fn new() -> Self {
|
||||
Self { out: [Cell::from_f64(0.0)] }
|
||||
}
|
||||
pub fn builder() -> PrimitiveBuilder {
|
||||
PrimitiveBuilder::new(
|
||||
"Sqrt",
|
||||
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(Sqrt::new()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Sqrt {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
|
||||
impl Node for Sqrt {
|
||||
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].max(0.0).sqrt());
|
||||
Some(&self.out)
|
||||
}
|
||||
fn label(&self) -> String { "Sqrt".to_string() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use aura_core::{AnyColumn, Scalar, Timestamp};
|
||||
|
||||
#[test]
|
||||
fn sqrt_of_nine_is_three_zero_is_zero() {
|
||||
let mut s = Sqrt::new();
|
||||
let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
|
||||
inputs[0].push(Scalar::f64(9.0)).unwrap();
|
||||
assert_eq!(s.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(3.0)].as_slice()));
|
||||
inputs[0].push(Scalar::f64(0.0)).unwrap();
|
||||
assert_eq!(s.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(0.0)].as_slice()));
|
||||
}
|
||||
#[test]
|
||||
fn sqrt_clamps_negative_to_zero() {
|
||||
let mut s = Sqrt::new();
|
||||
let mut inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
|
||||
inputs[0].push(Scalar::f64(-1e-12)).unwrap();
|
||||
assert_eq!(s.eval(Ctx::new(&inputs, Timestamp(0))), Some([Cell::from_f64(0.0)].as_slice()));
|
||||
}
|
||||
#[test]
|
||||
fn sqrt_is_none_until_input_present() {
|
||||
let mut s = Sqrt::new();
|
||||
let inputs = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
|
||||
assert_eq!(s.eval(Ctx::new(&inputs, Timestamp(0))), None);
|
||||
}
|
||||
}
|
||||
@@ -218,13 +218,46 @@ Add `mod sqrt;` (alphabetical, after `mod sma;` / before `mod stop_rule;`) and
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Remove the fused `VolStop` node (keep `FixedStop`)
|
||||
## Task 3: Remove the fused `VolStop` node (keep `FixedStop`) + migrate its one caller
|
||||
|
||||
`VolStop` IS referenced outside `stop_rule.rs`/`lib.rs`: `crates/aura-engine/tests/stage1_r_e2e.rs`
|
||||
imports it (line 30) and constructs `VolStop::new(1, 1.0)` in
|
||||
`r_is_stop_defined_two_stops_fold_to_different_expectancy`. That caller must be migrated
|
||||
in this task or the workspace build breaks.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/aura-std/src/stop_rule.rs`
|
||||
- Modify: `crates/aura-std/src/lib.rs`
|
||||
- Modify: `crates/aura-engine/tests/stage1_r_e2e.rs`
|
||||
|
||||
- [ ] **Step 1: Delete `VolStop` from `stop_rule.rs`**
|
||||
- [ ] **Step 1: Migrate the `stage1_r_e2e.rs` caller off `VolStop` (FIRST)**
|
||||
|
||||
`run_chain` drives a `&mut dyn Node` directly, so the vol stop (now a bootstrapped
|
||||
composite, Task 4) cannot slot in there. The test's property — "different stop distances
|
||||
fold to different R" — holds via two CONSTANT stops (R = (exit−entry)/distance, so a
|
||||
10×-tighter stop folds to a ~10× deeper R-loss). Replace the `VolStop` arm with a tight
|
||||
`FixedStop`:
|
||||
- line 30 import → `use aura_std::{FixedStop, PM_FIELD_NAMES, PM_RECORD_KINDS, PM_WIDTH, PositionManagement};` (drop `VolStop`).
|
||||
- in `r_is_stop_defined_two_stops_fold_to_different_expectancy`, replace the lines from
|
||||
`let mut vol = VolStop::new(1, 1.0);` through the final `assert!` with:
|
||||
|
||||
```rust
|
||||
// A TIGHT FixedStop (distance 1.0) vs the wide one (10.0): R = (exit-entry)/distance,
|
||||
// so the 10x-tighter stop folds to a ~10x deeper R-loss for a comparable drop.
|
||||
let mut tight = FixedStop::new(1.0);
|
||||
let tight_m = run_chain(&mut tight, &long_path(&[100.0, 100.0, 96.0]));
|
||||
assert!(wide.n_trades >= 1 && tight_m.n_trades >= 1);
|
||||
assert!(
|
||||
tight_m.expectancy_r < wide.expectancy_r,
|
||||
"stop choice must change folded R: tight={:?} wide={:?}",
|
||||
tight_m.expectancy_r,
|
||||
wide.expectancy_r,
|
||||
);
|
||||
```
|
||||
Update the test's doc-comment "tight VolStop" → "tight FixedStop". (The vol stop's
|
||||
varying-distance behaviour is covered by `vol_stop_composite.rs`, Task 4.)
|
||||
|
||||
- [ ] **Step 2: Delete `VolStop` from `stop_rule.rs`**
|
||||
|
||||
Remove the entire `pub struct VolStop`, its `impl VolStop`, its `impl Node for VolStop`,
|
||||
and the two inline tests `vol_stop_is_none_until_warm` and
|
||||
@@ -235,18 +268,17 @@ the module doc-comment: the volatility stop is now a composition (see
|
||||
`crates/aura-engine/tests/vol_stop_composite.rs`), `FixedStop` the only stop-rule
|
||||
primitive.
|
||||
|
||||
- [ ] **Step 2: Drop the `VolStop` re-export**
|
||||
- [ ] **Step 3: Drop the `VolStop` re-export**
|
||||
|
||||
In `crates/aura-std/src/lib.rs`: change `pub use stop_rule::{FixedStop, VolStop};` to
|
||||
`pub use stop_rule::FixedStop;`.
|
||||
|
||||
- [ ] **Step 3: Build the workspace green-unchanged**
|
||||
- [ ] **Step 4: Build the workspace green**
|
||||
|
||||
Run: `cargo build --workspace 2>&1 | grep -E "error|cannot find VolStop" | head`
|
||||
Expected: empty (no code outside `stop_rule.rs`/`lib.rs` references `VolStop` — the iter-1
|
||||
E2E uses `FixedStop`). Then `cargo test -p aura-std stop_rule` Expected: PASS
|
||||
(`fixed_stop_is_constant_after_first_price`). Then `cargo test --workspace 2>&1 | tail -5`
|
||||
Expected: all green.
|
||||
Run: `cargo build --workspace 2>&1 | grep -E "error" | head`
|
||||
Expected: empty. Then `cargo test --workspace 2>&1 | tail -8`
|
||||
Expected: all green (incl. the migrated
|
||||
`r_is_stop_defined_two_stops_fold_to_different_expectancy`).
|
||||
|
||||
---
|
||||
|
||||
@@ -345,9 +377,10 @@ Run: `cargo clippy --workspace --all-targets -- -D warnings` Expected: clean.
|
||||
Task 4 match the builders the implementer verifies.
|
||||
- **Step granularity:** each step 2-5 min; Tasks 1-2 are patterned mirrors of `sub.rs`.
|
||||
- **No commit steps:** none.
|
||||
- **Compile-gate ordering:** Task 3's `VolStop` removal threads its only references
|
||||
(`stop_rule.rs` + the `lib.rs` re-export) within Task 3; the workspace build gate is
|
||||
satisfiable because no other code references `VolStop` (iter-1 E2E uses `FixedStop`).
|
||||
- **Compile-gate ordering:** `VolStop` IS referenced by `stage1_r_e2e.rs`
|
||||
(`VolStop::new(1, 1.0)`), so Task 3 threads all three references — `stop_rule.rs`, the
|
||||
`lib.rs` re-export, AND the E2E caller (migrated to a tight `FixedStop` in Step 1, BEFORE
|
||||
the deletion) — within Task 3; the workspace build gate at Step 4 is then satisfiable.
|
||||
- **Verification filters resolve:** `cargo test -p aura-std mul` / `sqrt` /
|
||||
`stop_rule` / `-p aura-engine --test vol_stop_composite` match the new/kept named
|
||||
tests; the removal gate uses `cargo build --workspace` + the unfiltered suite.
|
||||
|
||||
Reference in New Issue
Block a user