4928e289f7
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
176 lines
6.2 KiB
Rust
176 lines
6.2 KiB
Rust
//! Runnable World-family demo: **`sweep`** the GER40 session-breakout `Composite`
|
||
//! blueprint over its two genuine tuning knobs — `entry_bar.target` ×
|
||
//! `exit_bar.target` — on REAL GER40 M1 bars, ranked by `total_pips`. This is the
|
||
//! core proof that the shipped strategy is World-consumable: the SAME
|
||
//! `ger40_breakout_blueprint` the single backtest bootstraps is driven through the
|
||
//! engine's `sweep` machinery with NO re-authoring (the #94 friction is gone), its
|
||
//! `param_space()` exercised directly.
|
||
//!
|
||
//! Run:
|
||
//! ```text
|
||
//! cargo run -p aura-ingest --example ger40_breakout_sweep
|
||
//! ```
|
||
//! Prints the ranked entry×exit grid (best `total_pips` first) over a full-year
|
||
//! 2024 window. Skips cleanly (no panic) where the archive is absent.
|
||
|
||
use std::sync::Arc;
|
||
|
||
use aura_core::{Cell, Scalar, Timestamp};
|
||
use aura_engine::{sweep, summarize, GridSpace, RunManifest, RunReport};
|
||
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 sweep grid: entry bar ∈ {2,3,4}, exit bar ∈ {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 a `RunReport` (`summarize` over the drained channels).
|
||
/// A fresh blueprint per call (fresh nodes + channels) keeps the disjoint sweep
|
||
/// points independent (C1).
|
||
fn run_point(server: &Arc<DataServer>, point: &[Cell], from: Timestamp, to: Timestamp) -> RunReport {
|
||
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("sweep 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();
|
||
RunReport {
|
||
manifest: RunManifest {
|
||
commit: "ger40-breakout-sweep".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),
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// Full-year 2024.
|
||
let (from, _) = utc_month_window(2024, 1);
|
||
let (_, to) = utc_month_window(2024, 12);
|
||
|
||
let pip_size = pip_size_of(SYMBOL);
|
||
|
||
// The grid is built from the blueprint's param_space() directly — the two
|
||
// EqConst targets are the swept axes (no re-authoring).
|
||
let space = ger40_breakout_blueprint(pip_size, BAR_MINUTES, SESSION_HOUR, SESSION_MINUTE, Berlin)
|
||
.0
|
||
.param_space();
|
||
let grid = GridSpace::new(
|
||
&space,
|
||
space
|
||
.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("grid well-formed for the breakout param_space");
|
||
|
||
println!("=== GER40 session-breakout — World `sweep` over entry×exit ===");
|
||
println!(
|
||
"symbol={SYMBOL} window=2024 (UTC, full year) bars=15m pip_size={pip_size} \
|
||
grid={}×{} = {} points",
|
||
ENTRY_BARS.len(),
|
||
EXIT_BARS.len(),
|
||
grid.len(),
|
||
);
|
||
println!("param_space() (the only swept knobs): {:?}", space.iter().map(|p| p.name.as_str()).collect::<Vec<_>>());
|
||
println!();
|
||
|
||
let server_for_closure = Arc::clone(&server);
|
||
let family = sweep(&grid, |pt: &[Cell]| {
|
||
run_point(&server_for_closure, pt, from, to)
|
||
});
|
||
|
||
// Rank the family by total_pips, descending.
|
||
let mut ranked: Vec<usize> = (0..family.points.len()).collect();
|
||
ranked.sort_by(|&a, &b| {
|
||
family.points[b]
|
||
.report
|
||
.metrics
|
||
.total_pips
|
||
.partial_cmp(&family.points[a].report.metrics.total_pips)
|
||
.unwrap_or(std::cmp::Ordering::Equal)
|
||
});
|
||
|
||
println!(
|
||
"{:>4} {:<10} {:<9} {:>12} {:>10} {:>7}",
|
||
"rank", "entry_bar", "exit_bar", "total_pips", "max_dd", "flips"
|
||
);
|
||
println!("{}", "-".repeat(58));
|
||
for (rank, &i) in ranked.iter().enumerate() {
|
||
let named = family.named_params(i);
|
||
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 = &family.points[i].report.metrics;
|
||
println!(
|
||
"{:>4} {:<10} {:<9} {:>12.1} {:>10.1} {:>7}",
|
||
rank + 1,
|
||
entry,
|
||
exit,
|
||
m.total_pips,
|
||
m.max_drawdown,
|
||
m.bias_sign_flips,
|
||
);
|
||
}
|
||
println!();
|
||
let best = ranked[0];
|
||
let best_named = family.named_params(best);
|
||
println!(
|
||
"best point: {:?} total_pips={:.1}",
|
||
best_named
|
||
.iter()
|
||
.map(|(n, v)| format!("{}={}", n.replace(".target", ""), v.as_i64()))
|
||
.collect::<Vec<_>>(),
|
||
family.points[best].report.metrics.total_pips,
|
||
);
|
||
println!("\nOK: the shipped breakout blueprint was swept over its param_space() — no re-authoring.");
|
||
}
|
||
|
||
fn as_f64(s: &Scalar) -> f64 {
|
||
match s {
|
||
Scalar::F64(v) => *v,
|
||
other => panic!("expected f64, got {other:?}"),
|
||
}
|
||
}
|