Files
Aura/crates/aura-ingest/examples/ger40_breakout_walkforward.rs
T
Brummel 4928e289f7 feat(project): the project-as-crate load boundary (cycle 0102)
A research project is now a loadable external cdylib crate. Inside a
directory whose ancestry holds an Aura.toml, aura discovers the project
root cargo-style, locates the compiled dylib via cargo metadata (debug
default, --release opt-in), loads it load-and-hold, and refuses
mismatches before trusting anything: the AURA_PROJECT descriptor
(aura-core::project, #[repr(C)]) carries a C-ABI stamp prefix (rustc +
aura-core version, baked per consuming build by the new aura-core
build.rs) validated before any Rust-ABI field is read. The vocabulary
charter gates the merged resolution: project type ids are ::-namespaced
(std stays bare), duplicates refuse, and the enumerable type-id list
must agree with the resolver, so introspection can never silently omit
a project type.

All blueprint verbs resolve through the merged project + std vocabulary
via a per-invocation Env threaded through the dispatch chains;
registry, trace-store, and data paths anchor at the project runs root
(Aura.toml [paths], paths-only by design — instrument geometry stays
the recorded sidecar, C15). RunManifest gains the Tier-1 project
provenance field (namespace + dylib sha256 + best-effort commit),
stamped beside topology_hash on the blueprint-run paths; pre-0102
registry lines load unchanged. Default node names strip the namespace,
so :: never reaches the param-path address space.

Proven by the demo-project fixture (built by the e2e via cargo,
path-dep on this workspace): run twice bit-identical, provenance
recorded, introspection lists demo::* beside std, registry anchors at
the discovered root from a subdirectory; the badcharter fixture proves
the charter refusal through the real libloading path; a never-built
project refuses with a cargo-build hint. Outside a project every path
collapses to the previous literals — goldens and manifest pins
byte-identical.

Verification: cargo build --workspace clean; cargo test --workspace 862
passed / 0 failed (incl. 7 project_load e2e); clippy -D warnings clean
(one precedent-matching allow(too_many_arguments) on run_oos_blueprint,
whose arity the Env threading raised to 8); doc build unchanged.
Docs/ledger aligned: Aura.toml field lists are paths-only in
project-layout.md, glossary, C16/C17; new C13 realization note records
the per-invocation-reload reading and the load-and-hold one-shot scope
boundary.

New deps, per-case review (aura-cli leaf binary only, never the frozen
artifact): libloading, toml.

refs #180
2026-07-02 18:13:37 +02:00

236 lines
8.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Runnable World-family demo: **`walk_forward`** the GER40 session-breakout
//! `Composite` blueprint across multi-year real GER40 history, with a
//! per-window in-sample optimize of its two genuine tuning knobs
//! (`entry_bar.target` × `exit_bar.target`). This is the executable #97
//! resolution: the roll is NON-DEGENERATE — a real, non-empty `param_space()` is
//! optimized per window, so every window's `chosen_params` is populated (contrast
//! the earlier fieldtest's empty-space degenerate roll over the raw FlatGraph, on
//! which no in-sample tuning was possible).
//!
//! Run:
//! ```text
//! cargo run -p aura-ingest --example ger40_breakout_walkforward
//! ```
//! Prints each IS/OOS window with the entry/exit it chose in-sample and its OOS
//! pips, plus the stitched out-of-sample equity curve. Skips cleanly where the
//! archive is absent.
use std::sync::Arc;
use aura_core::{Cell, Scalar, Timestamp};
use aura_engine::{
sweep, summarize, walk_forward, GridSpace, RollMode, RunManifest, RunReport, WindowBounds,
WindowRoller, WindowRun,
};
use chrono::TimeZone;
use chrono_tz::Europe::Berlin;
use aura_ingest::{default_data_server, open_ohlc, DataServer, DEFAULT_DATA_PATH};
#[path = "shared/breakout_real.rs"]
mod breakout_real;
use breakout_real::*;
// The in-sample grid the per-window optimize searches: entry ∈ {2,3,4}, exit ∈
// {4,5,6} — the two EqConst targets that ARE the blueprint's param_space().
const ENTRY_BARS: &[i64] = &[2, 3, 4];
const EXIT_BARS: &[i64] = &[4, 5, 6];
/// Bootstrap the blueprint at one grid point, run it over the epoch-ns
/// [`Timestamp`] window `[from, to]` of real GER40 OHLC, and fold the recorded
/// equity/held taps into `(RunReport, equity-segment)`. Fresh blueprint per call
/// (C1).
fn run_point(
server: &Arc<DataServer>,
point: &[Cell],
from: Timestamp,
to: Timestamp,
) -> (RunReport, Vec<(Timestamp, f64)>) {
let (bp, taps) = ger40_breakout_blueprint(
pip_size_of(SYMBOL),
BAR_MINUTES,
SESSION_HOUR,
SESSION_MINUTE,
Berlin,
);
let mut h = bp
.bootstrap_with_cells(point)
.expect("point kind-checked against param_space");
let sources =
open_ohlc(server, SYMBOL, from, to).expect("window overlaps GER40 data");
h.run(sources);
drop(h);
let equity: Vec<(Timestamp, f64)> = taps
.equity
.try_iter()
.map(|(t, r)| (t, as_f64(&r[0])))
.collect();
let exposure: Vec<(Timestamp, f64)> = taps
.held
.try_iter()
.map(|(t, r)| (t, as_f64(&r[0])))
.collect();
let report = RunReport {
manifest: RunManifest {
commit: "ger40-breakout-wfo".to_string(),
params: vec![],
window: (from, to),
seed: 0,
broker: "sim-optimal(pip_size=1)".to_string(),
selection: None,
instrument: None,
topology_hash: None,
project: None,
},
metrics: summarize(&equity, &exposure),
};
(report, equity)
}
fn main() {
let server = default_data_server();
if !server.has_symbol(SYMBOL) {
println!("skip: no local data at {DEFAULT_DATA_PATH} (symbol {SYMBOL} absent).");
return;
}
println!("=== GER40 session-breakout — World `walk_forward` (2018..2024) ===");
// The roll span: 2018-01 .. 2024-12. is=12mo, oos=3mo, step=3mo.
let day_ns: i64 = 86_400_000 * 1_000_000;
let month_ns: i64 = 30 * day_ns;
let origin = aura_ingest::unix_ms_to_epoch_ns(
chrono::Utc.with_ymd_and_hms(2018, 1, 1, 0, 0, 0).unwrap().timestamp_millis(),
);
let end = aura_ingest::unix_ms_to_epoch_ns(
chrono::Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap().timestamp_millis(),
);
let roller = WindowRoller::new(
(origin, end),
12 * month_ns,
3 * month_ns,
3 * month_ns,
RollMode::Rolling,
)
.expect("span fits >= 1 window");
// The non-empty space the per-window in-sample sweep optimizes over: the two
// genuine tuning knobs (the #97 resolution — a real space to optimize).
let space = ger40_breakout_blueprint(
pip_size_of(SYMBOL),
BAR_MINUTES,
SESSION_HOUR,
SESSION_MINUTE,
Berlin,
)
.0
.param_space();
println!(
"optimizing param_space() per window: {:?} (in-sample grid {}×{})",
space.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(),
ENTRY_BARS.len(),
EXIT_BARS.len(),
);
println!();
let server_for_closure = Arc::clone(&server);
let space_for_closure = space.clone();
let result = walk_forward(roller, space, move |w: WindowBounds| -> WindowRun {
// In-sample optimize: sweep the entry×exit grid over the IS window, choose
// the point with the best in-sample total_pips. The blueprint is consumed
// directly via its param_space() — no re-authoring.
let is_grid = GridSpace::new(
&space_for_closure,
space_for_closure
.iter()
.map(|ps| match ps.name.as_str() {
"entry_bar.target" => ENTRY_BARS.iter().map(|&v| Scalar::i64(v)).collect(),
"exit_bar.target" => EXIT_BARS.iter().map(|&v| Scalar::i64(v)).collect(),
other => panic!("unexpected param slot {other}"),
})
.collect(),
)
.expect("IS grid well-formed");
let is_family = sweep(&is_grid, |pt: &[Cell]| {
run_point(&server_for_closure, pt, w.is.0, w.is.1).0
});
let best = is_family
.points
.iter()
.max_by(|a, b| {
a.report
.metrics
.total_pips
.partial_cmp(&b.report.metrics.total_pips)
.unwrap_or(std::cmp::Ordering::Equal)
})
.expect("the IS grid is non-empty");
let chosen = best.params.clone();
// Apply the chosen params on the OOS window — the honest out-of-sample run.
let (oos_report, oos_equity) =
run_point(&server_for_closure, &chosen, w.oos.0, w.oos.1);
WindowRun { chosen_params: chosen, oos_equity, oos_report }
});
println!("walk-forward produced {} windows\n", result.windows.len());
println!(
"{:>3} {:<23} {:<10} {:<9} {:>10} {:>9}",
"win", "oos-window (UTC date)", "entry_bar", "exit_bar", "oos_pips", "oos_dd"
);
println!("{}", "-".repeat(72));
for (k, w) in result.windows.iter().enumerate() {
let named = result.named_params(k);
let entry = named.iter().find(|(n, _)| n == "entry_bar.target").map(|(_, v)| v.as_i64()).unwrap_or(-1);
let exit = named.iter().find(|(n, _)| n == "exit_bar.target").map(|(_, v)| v.as_i64()).unwrap_or(-1);
let m = &w.run.oos_report.metrics;
let win = format!(
"{}..{}",
fmt_date(w.bounds.oos.0),
fmt_date(w.bounds.oos.1),
);
println!(
"{:>3} {:<23} {:<10} {:<9} {:>10.1} {:>9.1}",
k,
win,
entry,
exit,
m.total_pips,
m.max_drawdown,
);
}
let stitched = &result.stitched_oos_equity;
println!(
"\nstitched OOS curve: {} points, final cumulative = {:.1} pips",
stitched.len(),
stitched.last().map(|(_, v)| *v).unwrap_or(0.0),
);
println!(
"\nOK: NON-DEGENERATE walk-forward — every window optimized the breakout's\n\
{}-param space in-sample (chosen_params populated). The #97 empty-space\n\
degenerate roll is resolved: the strategy ships with a real space to tune.",
space_param_count(&result.space),
);
}
fn space_param_count(space: &[aura_engine::ParamSpec]) -> usize {
space.len()
}
/// Render an epoch-ns timestamp as a UTC `YYYY-MM-DD` for the window column.
/// Display only — never feeds the graph.
fn fmt_date(ts: Timestamp) -> String {
let secs = ts.0.div_euclid(1_000_000_000);
match chrono::Utc.timestamp_opt(secs, 0).single() {
Some(dt) => dt.format("%Y-%m-%d").to_string(),
None => format!("ts={}", ts.0),
}
}
fn as_f64(s: &Scalar) -> f64 {
match s {
Scalar::F64(v) => *v,
other => panic!("expected f64, got {other:?}"),
}
}