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