0998f9aabf
Applies the principle the user named: a node is a primitive only if it is NOT DAG-expressible from other primitives; a function that needs a missing primitive gets the primitive added, not fused away. - Remove the fused VolStop node (it was pure feed-forward arithmetic). - The volatility stop is now a composition of primitives, vol_stop(length, k): k * Sqrt(Ema(Mul(d,d), length)), d = Sub(price, Delay(price,1)) -- a rolling EWMA standard deviation. Built with GraphBuilder; proven end-to-end (bootstraps + runs + emits k*sigma) in tests/vol_stop_composite.rs. - Migrate the one VolStop caller (stage1_r_e2e.rs) off it: the R-is-stop-defined test now contrasts a tight vs a wide FixedStop (two constant distances still fold to different R, the property under test). Uses the Mul + Sqrt primitives added in the prior commit. FixedStop stays the only stop-rule primitive (a triggered constant). Full suite + clippy green. refs #117 #119
74 lines
2.8 KiB
Rust
74 lines
2.8 KiB
Rust
//! Stop-rule nodes — emit a protective-stop *distance* (price units, ≥ 0,
|
|
//! direction-agnostic). The stop DEFINES the risk unit R (1R = the loss if stopped);
|
|
//! position-management latches the entry-cycle distance as the frozen R-denominator.
|
|
//!
|
|
//! `FixedStop` is the only stop-rule PRIMITIVE: a constant distance gated on its price
|
|
//! input (a source-less `Const` has no firing trigger in the push model, so the
|
|
//! triggered-constant shape is the honest primitive). The volatility stop is NOT a
|
|
//! primitive — it is a COMPOSITION of primitives, `k · Sqrt(Ema(Mul(Δ,Δ), length))`
|
|
//! with `Δ = Sub(price, Delay(price,1))` (a rolling EWMA standard deviation), built with
|
|
//! `GraphBuilder` — see `crates/aura-engine/tests/vol_stop_composite.rs`. True-range ATR
|
|
//! (a richer stop) is deferred — it needs OHLC.
|
|
use aura_core::{
|
|
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
|
|
ScalarKind,
|
|
};
|
|
|
|
/// Constant stop distance, gated on a price input. `distance` must be > 0.
|
|
pub struct FixedStop {
|
|
distance: f64,
|
|
out: [Cell; 1],
|
|
}
|
|
impl FixedStop {
|
|
pub fn new(distance: f64) -> Self {
|
|
assert!(distance > 0.0, "FixedStop distance must be > 0");
|
|
Self { distance, out: [Cell::from_f64(distance)] }
|
|
}
|
|
pub fn builder() -> PrimitiveBuilder {
|
|
PrimitiveBuilder::new(
|
|
"FixedStop",
|
|
NodeSchema {
|
|
inputs: vec![PortSpec { kind: ScalarKind::F64, firing: Firing::Any, name: "price".into() }],
|
|
output: vec![FieldSpec { name: "stop_distance".into(), kind: ScalarKind::F64 }],
|
|
params: vec![ParamSpec { name: "distance".into(), kind: ScalarKind::F64 }],
|
|
},
|
|
|p| Box::new(FixedStop::new(p[0].f64())),
|
|
)
|
|
}
|
|
}
|
|
impl Node for FixedStop {
|
|
fn lookbacks(&self) -> Vec<usize> {
|
|
vec![1]
|
|
}
|
|
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
|
|
if ctx.f64_in(0).is_empty() {
|
|
return None; // fire with price (warm-up filter)
|
|
}
|
|
self.out[0] = Cell::from_f64(self.distance);
|
|
Some(&self.out)
|
|
}
|
|
fn label(&self) -> String {
|
|
format!("FixedStop({})", self.distance)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use aura_core::{AnyColumn, Scalar, Timestamp};
|
|
fn feed(node: &mut dyn Node, prices: &[f64]) -> Vec<Option<f64>> {
|
|
let mut col = vec![AnyColumn::with_capacity(ScalarKind::F64, 1)];
|
|
let mut out = vec![];
|
|
for &p in prices {
|
|
col[0].push(Scalar::f64(p)).unwrap();
|
|
out.push(node.eval(Ctx::new(&col, Timestamp(0))).map(|c| c[0].f64()));
|
|
}
|
|
out
|
|
}
|
|
#[test]
|
|
fn fixed_stop_is_constant_after_first_price() {
|
|
let mut s = FixedStop::new(2.5);
|
|
assert_eq!(feed(&mut s, &[100.0, 110.0, 90.0]), vec![Some(2.5), Some(2.5), Some(2.5)]);
|
|
}
|
|
}
|