fieldtest: milestone-33 — 4 examples, 11 findings

Milestone fieldtest for "Real-project readiness — role-complete
research loop (no freeze)": a from-scratch ger40-lab project (aura new)
with a brand-new native node (ger40_lab::Momentum) driven through the
full research loop on real GER40/FRA40 archive data — sweep,
walk-forward, mc, process+campaign with 2-regime risk and net-R cost
block, cross-instrument generalize, bit-identical reproduce, charts —
from the public interface only. Spine holds: 5 working, 1 bug (chart
cannot open a sweep --trace family by its printed handle), 4 friction,
1 spec gap. Findings triaged separately.
This commit is contained in:
2026-07-11 14:20:48 +02:00
parent d3ad9f4e73
commit 09da04fe18
10 changed files with 203 additions and 0 deletions
@@ -0,0 +1,3 @@
/target
Cargo.lock
/runs
@@ -0,0 +1,4 @@
# Static project context only (C17); paths only.
[paths]
runs = "runs"
# data = "/path/to/archive" # the recorded-data root; defaults to the built-in path
@@ -0,0 +1,10 @@
# ger40-lab — an aura research project
This crate is an aura project: a cdylib of node/strategy blueprints the
`aura` host loads during research (see the engine's docs/project-layout.md).
- Build: `cargo build` (the next `aura` invocation loads the fresh dylib)
- Run: `aura run blueprints/signal.json` (from anywhere inside this dir)
- Nodes live in `src/`; each is registered in `vocabulary()`/`type_ids()`
under the `ger40_lab::` namespace prefix.
- Topology is data (`blueprints/*.json`), node logic is Rust.
@@ -0,0 +1,14 @@
[package]
name = "ger40-lab"
version = "0.1.0"
edition = "2024"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
aura-core = { path = "/home/brummel/dev/aura/.claude/worktrees/boss-real-project-readiness/crates/aura-core" }
# Standalone workspace root: never a member of an enclosing workspace.
[workspace]
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"graph","nodes":[{"primitive":{"type":"ger40_lab::Momentum","name":"mom"}},{"primitive":{"type":"Bias","name":"bias"}}],"edges":[{"from":0,"to":1,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0}],"source":"F64"}],"output":[{"node":1,"field":0,"name":"bias"}]}}
@@ -0,0 +1,8 @@
[
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "ger40_lab::Momentum", "name": "mom"},
{"op": "feed", "role": "price", "into": ["mom.series"]},
{"op": "add", "type": "Bias", "name": "bias"},
{"op": "connect", "from": "mom.value", "to": "bias.signal"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"ger40_lab_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}},{"primitive":{"type":"ger40_lab::Scale","bound":[{"pos":0,"name":"factor","kind":"F64","value":{"F64":1.0}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":4,"field":0,"name":"bias"}]}}
@@ -0,0 +1,29 @@
{
"format_version": 1,
"kind": "campaign",
"name": "mom-ger40-fra40-net-r",
"seed": 42,
"data": {
"instruments": ["GER40", "FRA40"],
"windows": [ { "from_ms": 1725148800000, "to_ms": 1727740800000 } ]
},
"risk": [
{ "vol": { "length": 3, "k": 1.5 } },
{ "vol": { "length": 3, "k": 3.0 } }
],
"cost": [
{ "constant": { "cost_per_trade": 0.02 } },
{ "vol_slippage": { "slip_vol_mult": 0.1 } }
],
"strategies": [
{
"ref": { "content_id": "0a077701913753e745650f2d2d439980366acd2bcb77e995f8f75be971bbea0d" },
"axes": {
"mom.length": { "kind": "I64", "values": [5, 10] },
"bias.scale": { "kind": "F64", "values": [1.0] }
}
}
],
"process": { "ref": { "content_id": "639bc1b1db0ffa87f1bcc16a6790e0119a9d0968237c8639899fa75c7390df73" } },
"presentation": { "persist_taps": ["equity", "r_equity", "net_r_equity"], "emit": ["family_table", "selection_report"] }
}
@@ -0,0 +1,11 @@
{
"format_version": 1,
"kind": "process",
"name": "mom-sweep-gate-generalize",
"description": "Sweep the momentum axis, gate on positive gross expectancy, floor across instruments.",
"pipeline": [
{ "block": "std::sweep", "metric": "sqn", "select": "argmax", "deflate": true },
{ "block": "std::gate", "all": [ { "metric": "expectancy_r", "cmp": "gt", "value": 0.0 } ] },
{ "block": "std::generalize", "metric": "expectancy_r" }
]
}
@@ -0,0 +1,122 @@
//! ger40-lab — an aura research project: node logic + the exported vocabulary.
//!
//! Every node type this crate provides is registered in `vocabulary()` /
//! `type_ids()` under the `ger40_lab::` namespace prefix; the `aura` host loads
//! this cdylib per invocation and merges it with the std vocabulary.
use aura_core::{
Cell, Ctx, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder,
ScalarKind,
};
/// One-input scalar gain: emits `input * factor`. Emits `None` until its
/// input has a value (warm-up filter, C8).
pub struct Scale {
factor: f64,
out: [Cell; 1],
}
impl Scale {
pub fn new(factor: f64) -> Self {
Self { factor, out: [Cell::from_f64(0.0)] }
}
pub fn builder() -> PrimitiveBuilder {
PrimitiveBuilder::new(
"ger40_lab::Scale",
NodeSchema {
inputs: vec![PortSpec {
kind: ScalarKind::F64,
firing: Firing::Any,
name: "value".into(),
}],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "factor".into(), kind: ScalarKind::F64 }],
},
|p| Box::new(Scale::new(p[0].f64())),
)
}
}
impl Node for Scale {
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] * self.factor);
Some(&self.out)
}
fn label(&self) -> String {
format!("ger40_lab::Scale({})", self.factor)
}
}
/// One-input momentum: emits `price_now - price_{length}_bars_ago`, i.e. the
/// raw change over `length` cycles. Positive => up-move, negative => down-move.
/// Reads a window of `length + 1` cycles (financial-style indexing, index 0 =
/// newest; design ledger C4/RingBuffer). Emits `None` until that much history
/// has accumulated (warm-up filter, C8).
pub struct Momentum {
length: usize,
out: [Cell; 1],
}
impl Momentum {
pub fn new(length: i64) -> Self {
Self { length: length.max(1) as usize, out: [Cell::from_f64(0.0)] }
}
pub fn builder() -> PrimitiveBuilder {
PrimitiveBuilder::new(
"ger40_lab::Momentum",
NodeSchema {
inputs: vec![PortSpec {
kind: ScalarKind::F64,
firing: Firing::Any,
name: "series".into(),
}],
output: vec![FieldSpec { name: "value".into(), kind: ScalarKind::F64 }],
params: vec![ParamSpec { name: "length".into(), kind: ScalarKind::I64 }],
},
|p| Box::new(Momentum::new(p[0].i64())),
)
}
}
impl Node for Momentum {
fn lookbacks(&self) -> Vec<usize> {
vec![self.length + 1]
}
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Cell]> {
let w = ctx.f64_in(0);
if w.len() <= self.length {
return None;
}
self.out[0] = Cell::from_f64(w[0] - w[self.length]);
Some(&self.out)
}
fn label(&self) -> String {
format!("ger40_lab::Momentum({})", self.length)
}
}
fn vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
match type_id {
"ger40_lab::Scale" => Some(Scale::builder()),
"ger40_lab::Momentum" => Some(Momentum::builder()),
_ => None,
}
}
fn type_ids() -> &'static [&'static str] {
&["ger40_lab::Scale", "ger40_lab::Momentum"]
}
aura_core::aura_project! {
namespace: "ger40_lab",
vocabulary: vocabulary,
type_ids: type_ids,
}