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:
@@ -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"}]}}
|
||||
+29
@@ -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"] }
|
||||
}
|
||||
+11
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user