refactor(cli): wrap_r declares its root roles from the resolved binding (#231 task 3)
Behaviour-preserving threading: wrap_r's hard-wired
source_role("price") weld is gone — root roles now come from a
ResolvedBinding, one per entry in canonical column order, each fed into
the signal's same-named port; the guaranteed close entry always feeds
broker + executor (whose 'price' is a node-schema port name, not a
role). Every existing blueprint resolves to the identical single-price
plan, so the entire suite is the gate: green unchanged.
Threaded through all callers: run_signal_r, run_blueprint_member (+ its
production and test callers), blueprint_axis_probe (lenient probe
binding), reproduce_family_in, the sweep/oos/walkforward/mc family
builders, CliMemberRunner::run_member, persist_campaign_traces.
Vocabulary role names use their static form at the port seam; only an
override-renamed role leaks its name once per graph build (post-review
polish capping the per-member leak).
Verified: full workspace suite green unchanged, clippy -D warnings
clean.
refs #231
This commit is contained in:
@@ -31,6 +31,23 @@ const RESERVED_CLOSE_ROLE: &str = "close";
|
|||||||
pub(crate) const COLUMN_VOCABULARY: &str =
|
pub(crate) const COLUMN_VOCABULARY: &str =
|
||||||
"open, high, low, close (alias: price), spread, volume";
|
"open, high, low, close (alias: price), spread, volume";
|
||||||
|
|
||||||
|
/// The `&'static str` form of a vocabulary role name, when it is one — the
|
||||||
|
/// common case at the wrap seam, so declaring a port for a vocabulary-named
|
||||||
|
/// role needs no per-build leak; only an override-renamed (non-vocabulary)
|
||||||
|
/// role falls back to leaking its name.
|
||||||
|
pub(crate) fn static_role_name(name: &str) -> Option<&'static str> {
|
||||||
|
match name {
|
||||||
|
"open" => Some("open"),
|
||||||
|
"high" => Some("high"),
|
||||||
|
"low" => Some("low"),
|
||||||
|
"close" => Some("close"),
|
||||||
|
"price" => Some("price"),
|
||||||
|
"spread" => Some("spread"),
|
||||||
|
"volume" => Some("volume"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The closed role-name -> archive-column map (`price` aliases `close`).
|
/// The closed role-name -> archive-column map (`price` aliases `close`).
|
||||||
pub(crate) fn column_for_role(name: &str) -> Option<M1Field> {
|
pub(crate) fn column_for_role(name: &str) -> Option<M1Field> {
|
||||||
match name {
|
match name {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
//! submodule idiom: main.rs is the crate root, so its private items are
|
//! submodule idiom: main.rs is the crate root, so its private items are
|
||||||
//! visible to child modules without promotion.
|
//! visible to child modules without promotion.
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{mpsc, Arc};
|
use std::sync::{mpsc, Arc};
|
||||||
|
|
||||||
@@ -241,6 +242,15 @@ impl MemberRunner for CliMemberRunner<'_> {
|
|||||||
let space = crate::blueprint_axis_probe(&cell.blueprint_json, self.env).param_space();
|
let space = crate::blueprint_axis_probe(&cell.blueprint_json, self.env).param_space();
|
||||||
let point = bind_axes(&space, &cell.strategy_id, params)?;
|
let point = bind_axes(&space, &cell.strategy_id, params)?;
|
||||||
|
|
||||||
|
// The member's blueprint + its resolved input binding (name defaults;
|
||||||
|
// the campaign's data.bindings overrides thread through in the
|
||||||
|
// DataSection task). A refusal is a member fault, never a process exit.
|
||||||
|
let signal = blueprint_from_json(&cell.blueprint_json, &|t| self.env.resolve(t))
|
||||||
|
.expect("stored blueprint passed the referential gate; reload is infallible");
|
||||||
|
let binding =
|
||||||
|
crate::binding::resolve_binding(&cell.strategy_id, signal.input_roles(), &BTreeMap::new())
|
||||||
|
.map_err(MemberFault::Bind)?;
|
||||||
|
|
||||||
// Real windowed data — geometry BEFORE bar data (the shipped pre-data
|
// Real windowed data — geometry BEFORE bar data (the shipped pre-data
|
||||||
// refusal order of `open_real_source`), both as member faults.
|
// refusal order of `open_real_source`), both as member faults.
|
||||||
let geo = instrument_geometry(&self.server, &cell.instrument).ok_or_else(|| {
|
let geo = instrument_geometry(&self.server, &cell.instrument).ok_or_else(|| {
|
||||||
@@ -279,12 +289,10 @@ impl MemberRunner for CliMemberRunner<'_> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// The shipped member recipe (reload — a Composite is !Clone — wrap,
|
// The shipped member recipe (wrap, bind, run, summarize):
|
||||||
// bind, run, summarize): `run_blueprint_member` verbatim. Seed 0
|
// `run_blueprint_member` verbatim. Seed 0 (seed-free real-data runs),
|
||||||
// (seed-free real-data runs), topology_hash = the strategy's content
|
// topology_hash = the strategy's content id, project provenance
|
||||||
// id, project provenance stamped inside the helper.
|
// stamped inside the helper.
|
||||||
let signal = blueprint_from_json(&cell.blueprint_json, &|t| self.env.resolve(t))
|
|
||||||
.expect("stored blueprint passed the referential gate; reload is infallible");
|
|
||||||
let stop = stop_rule_for_regime(cell.regime);
|
let stop = stop_rule_for_regime(cell.regime);
|
||||||
let mut report = crate::run_blueprint_member(
|
let mut report = crate::run_blueprint_member(
|
||||||
signal,
|
signal,
|
||||||
@@ -297,6 +305,7 @@ impl MemberRunner for CliMemberRunner<'_> {
|
|||||||
&cell.strategy_id,
|
&cell.strategy_id,
|
||||||
self.env,
|
self.env,
|
||||||
stop,
|
stop,
|
||||||
|
&binding,
|
||||||
);
|
);
|
||||||
report.manifest.instrument = Some(cell.instrument.clone());
|
report.manifest.instrument = Some(cell.instrument.clone());
|
||||||
Ok(report)
|
Ok(report)
|
||||||
@@ -827,6 +836,10 @@ pub(crate) fn persist_campaign_traces(
|
|||||||
}
|
}
|
||||||
let signal = blueprint_from_json(blueprint_json, &|t| env.resolve(t))
|
let signal = blueprint_from_json(blueprint_json, &|t| env.resolve(t))
|
||||||
.expect("stored blueprint passed the referential gate; reload is infallible");
|
.expect("stored blueprint passed the referential gate; reload is infallible");
|
||||||
|
// Same name-default binding the member ran under (campaign
|
||||||
|
// overrides thread through in the DataSection task).
|
||||||
|
let binding =
|
||||||
|
crate::binding::resolve_binding(&cell_rec.strategy, signal.input_roles(), &BTreeMap::new())?;
|
||||||
// The cell's OWN regime (#219/#212), not the hardcoded default: the
|
// The cell's OWN regime (#219/#212), not the hardcoded default: the
|
||||||
// recorded member ran under `cell_rec.regime`, bound through the same
|
// recorded member ran under `cell_rec.regime`, bound through the same
|
||||||
// `stop_rule_for_regime` helper `CliMemberRunner::run_member` uses, so
|
// `stop_rule_for_regime` helper `CliMemberRunner::run_member` uses, so
|
||||||
@@ -837,7 +850,7 @@ pub(crate) fn persist_campaign_traces(
|
|||||||
let (tx_ex, rx_ex) = mpsc::channel();
|
let (tx_ex, rx_ex) = mpsc::channel();
|
||||||
let (tx_r, rx_r) = mpsc::channel();
|
let (tx_r, rx_r) = mpsc::channel();
|
||||||
let (tx_req, rx_req) = mpsc::channel();
|
let (tx_req, rx_req) = mpsc::channel();
|
||||||
let mut h = crate::wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, stop, false, geo.pip_size)
|
let mut h = crate::wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, stop, false, geo.pip_size, &binding)
|
||||||
.bootstrap_with_cells(&point)
|
.bootstrap_with_cells(&point)
|
||||||
.expect("the member's point re-bootstraps (it already ran this realization)");
|
.expect("the member's point re-bootstraps (it already ran this realization)");
|
||||||
let sources: Vec<Box<dyn aura_engine::Source>> = vec![Box::new(source)];
|
let sources: Vec<Box<dyn aura_engine::Source>> = vec![Box::new(source)];
|
||||||
|
|||||||
+102
-33
@@ -68,7 +68,7 @@ use aura_std::{Bias, Ema, Sma};
|
|||||||
use aura_std::{RollingMax, RollingMin, Sub};
|
use aura_std::{RollingMax, RollingMin, Sub};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::collections::HashSet;
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
|
||||||
/// The pip size the built-in *synthetic* families (sweep/walkforward/mc over a
|
/// The pip size the built-in *synthetic* families (sweep/walkforward/mc over a
|
||||||
@@ -1111,7 +1111,14 @@ fn reproduce_family_in(
|
|||||||
let (tx_r, _) = mpsc::channel();
|
let (tx_r, _) = mpsc::channel();
|
||||||
let (tx_req, _) = mpsc::channel();
|
let (tx_req, _) = mpsc::channel();
|
||||||
let stop = stop_rule_from_params(&stored.manifest.params);
|
let stop = stop_rule_from_params(&stored.manifest.params);
|
||||||
let space = wrap_r(reload(), tx_eq, tx_ex, tx_r, tx_req, stop, true, SYNTHETIC_PIP_SIZE).param_space();
|
// The member's binding, re-derived from the stored blueprint's own
|
||||||
|
// input roles (name defaults — family manifests carry no overrides).
|
||||||
|
let binding = binding::resolve_binding(&hash, reload().input_roles(), &BTreeMap::new())
|
||||||
|
.unwrap_or_else(|m| {
|
||||||
|
eprintln!("aura: {m}");
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
let space = wrap_r(reload(), tx_eq, tx_ex, tx_r, tx_req, stop, true, SYNTHETIC_PIP_SIZE, &binding).param_space();
|
||||||
let point = point_from_params(&space, &stored.manifest.params);
|
let point = point_from_params(&space, &stored.manifest.params);
|
||||||
// A MonteCarlo member carries no tuning params (the params-join is empty), so its
|
// A MonteCarlo member carries no tuning params (the params-join is empty), so its
|
||||||
// reproduce line would print a BLANK member label; the seed IS its realization
|
// reproduce line would print a BLANK member label; the seed IS its realization
|
||||||
@@ -1172,6 +1179,7 @@ fn reproduce_family_in(
|
|||||||
&hash,
|
&hash,
|
||||||
env,
|
env,
|
||||||
stop,
|
stop,
|
||||||
|
&binding,
|
||||||
);
|
);
|
||||||
outcomes.push((label, rerun.metrics == stored.metrics));
|
outcomes.push((label, rerun.metrics == stored.metrics));
|
||||||
}
|
}
|
||||||
@@ -1265,11 +1273,14 @@ fn topology_hash(signal: &Composite) -> String {
|
|||||||
content_id(&blueprint_to_json(signal).expect("a buildable signal serializes"))
|
content_id(&blueprint_to_json(signal).expect("a buildable signal serializes"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrap a `signal` composite (a `price`→`bias` leg) in the R run
|
/// Wrap a `signal` composite (a roles→`bias` leg) in the R run
|
||||||
/// scaffolding: pip broker, the per-tap recorders, the vol-stop RiskExecutor, the
|
/// scaffolding: pip broker, the per-tap recorders, the vol-stop RiskExecutor, the
|
||||||
/// r_equity / cost legs. The signal is nested and its `price`/`bias` boundary is
|
/// r_equity / cost legs. The signal is nested; its root roles come from the
|
||||||
/// wired across; a serialized signal loaded via `blueprint_from_json` runs through
|
/// resolved `binding` (one per entry, canonical column order — the same order the
|
||||||
/// exactly the scaffolding the Rust-built signal does.
|
/// callers open real columns in, so role i receives source i), and the binding's
|
||||||
|
/// guaranteed close entry always feeds the broker/executor pair. A serialized
|
||||||
|
/// signal loaded via `blueprint_from_json` runs through exactly the scaffolding
|
||||||
|
/// the Rust-built signal does.
|
||||||
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
|
||||||
fn wrap_r(
|
fn wrap_r(
|
||||||
signal: Composite,
|
signal: Composite,
|
||||||
@@ -1280,9 +1291,10 @@ fn wrap_r(
|
|||||||
stop: StopRule,
|
stop: StopRule,
|
||||||
reduce: bool,
|
reduce: bool,
|
||||||
pip_size: f64,
|
pip_size: f64,
|
||||||
|
binding: &binding::ResolvedBinding,
|
||||||
) -> Composite {
|
) -> Composite {
|
||||||
let mut g = GraphBuilder::new("r_sma");
|
let mut g = GraphBuilder::new("r_sma");
|
||||||
// SMA-cross signal → Bias, nested as a serializable `price`→`bias` leg.
|
// The strategy signal → Bias, nested as a serializable roles→`bias` leg.
|
||||||
let sig = g.add(BlueprintNode::Composite(signal));
|
let sig = g.add(BlueprintNode::Composite(signal));
|
||||||
// pip branch (verbatim from the retired `sample_blueprint_with_sinks`, #159).
|
// pip branch (verbatim from the retired `sample_blueprint_with_sinks`, #159).
|
||||||
let broker = g.add(SimBroker::builder(pip_size));
|
let broker = g.add(SimBroker::builder(pip_size));
|
||||||
@@ -1313,13 +1325,32 @@ fn wrap_r(
|
|||||||
} else {
|
} else {
|
||||||
g.add(Recorder::builder(PM_RECORD_KINDS.to_vec(), Firing::Any, tx_r))
|
g.add(Recorder::builder(PM_RECORD_KINDS.to_vec(), Firing::Any, tx_r))
|
||||||
};
|
};
|
||||||
let price = g.source_role("price", ScalarKind::F64);
|
// Root roles come from the resolved binding, one per entry in canonical
|
||||||
let price_targets = vec![
|
// column order (the C4 merge tie-break order the callers open columns in).
|
||||||
sig.input("price"),
|
// The binding guarantees a close entry, which always feeds the broker and
|
||||||
broker.input("price"),
|
// the executor — "price" below is their node-schema PORT name, not a role
|
||||||
exec.input("price"),
|
// name. For a single-`price` signal this declares exactly the old weld:
|
||||||
];
|
// one role, targets [sig, broker, exec] (feed calls append targets).
|
||||||
g.feed(price, price_targets);
|
let mut close_handle = None;
|
||||||
|
for entry in binding.entries() {
|
||||||
|
let role = g.source_role(&entry.role, binding::column_kind(entry.column));
|
||||||
|
if entry.feeds_signal {
|
||||||
|
// `NodeHandle::input` takes a `&'static str` port name (the fluent
|
||||||
|
// `GraphBuilder`'s authoring-time contract, builder.rs) but the
|
||||||
|
// binding's role names are dynamic (loaded from a blueprint at
|
||||||
|
// runtime). Vocabulary names use their static form; only an
|
||||||
|
// override-renamed role leaks its name (once per graph build,
|
||||||
|
// never per-tick).
|
||||||
|
let port: &'static str = binding::static_role_name(&entry.role)
|
||||||
|
.unwrap_or_else(|| Box::leak(entry.role.clone().into_boxed_str()));
|
||||||
|
g.feed(role, [sig.input(port)]);
|
||||||
|
}
|
||||||
|
if entry.column == aura_ingest::M1Field::Close && close_handle.is_none() {
|
||||||
|
close_handle = Some(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let close = close_handle.expect("ResolvedBinding guarantees a close entry");
|
||||||
|
g.feed(close, [broker.input("price"), exec.input("price")]);
|
||||||
g.connect(sig.output("bias"), broker.input("exposure"));
|
g.connect(sig.output("bias"), broker.input("exposure"));
|
||||||
g.connect(sig.output("bias"), ex.input("col[0]"));
|
g.connect(sig.output("bias"), ex.input("col[0]"));
|
||||||
g.connect(sig.output("bias"), exec.input("bias"));
|
g.connect(sig.output("bias"), exec.input("bias"));
|
||||||
@@ -1378,6 +1409,13 @@ fn run_signal_r(
|
|||||||
signal: Composite, params: &[Scalar], data: RunData, seed: u64, env: &project::Env,
|
signal: Composite, params: &[Scalar], data: RunData, seed: u64, env: &project::Env,
|
||||||
) -> RunReport {
|
) -> RunReport {
|
||||||
let topo = topology_hash(&signal); // before signal is consumed
|
let topo = topology_hash(&signal); // before signal is consumed
|
||||||
|
// The default binding (name defaults; `aura run` carries no campaign
|
||||||
|
// overrides). Refusals are the established `aura: ` + exit-1 register.
|
||||||
|
let binding = binding::resolve_binding(signal.name(), signal.input_roles(), &BTreeMap::new())
|
||||||
|
.unwrap_or_else(|m| {
|
||||||
|
eprintln!("aura: {m}");
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
let names: Vec<String> = signal
|
let names: Vec<String> = signal
|
||||||
.param_space()
|
.param_space()
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1390,7 +1428,7 @@ fn run_signal_r(
|
|||||||
// receiver alive so the sink's sends do not fail, but do not drain it.
|
// receiver alive so the sink's sends do not fail, but do not drain it.
|
||||||
let (tx_req, _rx_req) = mpsc::channel();
|
let (tx_req, _rx_req) = mpsc::channel();
|
||||||
let (sources, window, pip_size) = resolve_run_data(&data, env);
|
let (sources, window, pip_size) = resolve_run_data(&data, env);
|
||||||
let wrapped = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, false, pip_size);
|
let wrapped = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, false, pip_size, &binding);
|
||||||
let flat = wrapped
|
let flat = wrapped
|
||||||
.compile_with_params(params)
|
.compile_with_params(params)
|
||||||
.expect("signal binds + wraps to a valid harness");
|
.expect("signal binds + wraps to a valid harness");
|
||||||
@@ -1428,12 +1466,13 @@ fn run_blueprint_member(
|
|||||||
topo: &str,
|
topo: &str,
|
||||||
env: &project::Env,
|
env: &project::Env,
|
||||||
stop: StopRule,
|
stop: StopRule,
|
||||||
|
binding: &binding::ResolvedBinding,
|
||||||
) -> RunReport {
|
) -> RunReport {
|
||||||
let (tx_eq, rx_eq) = mpsc::channel();
|
let (tx_eq, rx_eq) = mpsc::channel();
|
||||||
let (tx_ex, rx_ex) = mpsc::channel();
|
let (tx_ex, rx_ex) = mpsc::channel();
|
||||||
let (tx_r, rx_r) = mpsc::channel();
|
let (tx_r, rx_r) = mpsc::channel();
|
||||||
let (tx_req, _rx_req) = mpsc::channel();
|
let (tx_req, _rx_req) = mpsc::channel();
|
||||||
let mut h = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, stop, true, pip)
|
let mut h = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, stop, true, pip, binding)
|
||||||
.bootstrap_with_cells(point)
|
.bootstrap_with_cells(point)
|
||||||
.expect("member bootstraps (point kind-checked against param_space)");
|
.expect("member bootstraps (point kind-checked against param_space)");
|
||||||
h.run(sources);
|
h.run(sources);
|
||||||
@@ -1476,11 +1515,15 @@ fn run_blueprint_member(
|
|||||||
fn blueprint_axis_probe(doc: &str, env: &project::Env) -> Composite {
|
fn blueprint_axis_probe(doc: &str, env: &project::Env) -> Composite {
|
||||||
let signal = blueprint_from_json(doc, &|t| env.resolve(t))
|
let signal = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
|
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
|
||||||
|
// The PROBE binding is lenient (unresolvable roles fall back to close),
|
||||||
|
// like the probe's synthetic pip: the wrap is built for its param_space
|
||||||
|
// only, never run over data — strict resolution lives on the run paths.
|
||||||
|
let probe = binding::probe_binding(signal.input_roles());
|
||||||
let (tx_eq, _) = mpsc::channel();
|
let (tx_eq, _) = mpsc::channel();
|
||||||
let (tx_ex, _) = mpsc::channel();
|
let (tx_ex, _) = mpsc::channel();
|
||||||
let (tx_r, _) = mpsc::channel();
|
let (tx_r, _) = mpsc::channel();
|
||||||
let (tx_req, _) = mpsc::channel();
|
let (tx_req, _) = mpsc::channel();
|
||||||
wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, true, SYNTHETIC_PIP_SIZE)
|
wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, true, SYNTHETIC_PIP_SIZE, &probe)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `aura sweep <blueprint.json> --list-axes`: one `<name>:<kind>` line per open
|
/// `aura sweep <blueprint.json> --list-axes`: one `<name>:<kind>` line per open
|
||||||
@@ -1521,7 +1564,11 @@ fn blueprint_sweep_family(
|
|||||||
blueprint_from_json(d, &|t| env.resolve(t))
|
blueprint_from_json(d, &|t| env.resolve(t))
|
||||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
|
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
|
||||||
};
|
};
|
||||||
let topo = topology_hash(&reload(doc));
|
let probe_signal = reload(doc);
|
||||||
|
let topo = topology_hash(&probe_signal);
|
||||||
|
// Strict binding resolution (name defaults — the verb path carries no
|
||||||
|
// campaign overrides): the family's open plan and wrap plan in one value.
|
||||||
|
let binding = binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())?;
|
||||||
let pip = data.pip_size();
|
let pip = data.pip_size();
|
||||||
let window = data.full_window(env);
|
let window = data.full_window(env);
|
||||||
// a single throwaway floated build, only to resolve param_space (borrow) then seed
|
// a single throwaway floated build, only to resolve param_space (borrow) then seed
|
||||||
@@ -1554,7 +1601,7 @@ fn blueprint_sweep_family(
|
|||||||
.sweep(|point| {
|
.sweep(|point| {
|
||||||
// fresh per-member graph (Composite is !Clone, reload per member) run through
|
// fresh per-member graph (Composite is !Clone, reload per member) run through
|
||||||
// the shared reduce-mode member path — the same fn reproduction re-runs.
|
// the shared reduce-mode member path — the same fn reproduction re-runs.
|
||||||
run_blueprint_member(reload(doc), point, &space, data.run_sources(env), window, 0, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K })
|
run_blueprint_member(reload(doc), point, &space, data.run_sources(env), window, 0, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, &binding)
|
||||||
})
|
})
|
||||||
// render the sweep terminal's BindError to a message (the fn's String error contract),
|
// render the sweep terminal's BindError to a message (the fn's String error contract),
|
||||||
// so `UnknownKnob("nope")` still surfaces verbatim at the IO wrapper.
|
// so `UnknownKnob("nope")` still surfaces verbatim at the IO wrapper.
|
||||||
@@ -1568,7 +1615,7 @@ fn blueprint_sweep_family(
|
|||||||
/// sweep terminal (no panic, no hidden exit) for the caller to render.
|
/// sweep terminal (no panic, no hidden exit) for the caller to render.
|
||||||
fn blueprint_sweep_over(
|
fn blueprint_sweep_over(
|
||||||
doc: &str, axes: &[(String, Vec<Scalar>)], from: Timestamp, to: Timestamp, data: &DataSource,
|
doc: &str, axes: &[(String, Vec<Scalar>)], from: Timestamp, to: Timestamp, data: &DataSource,
|
||||||
env: &project::Env,
|
env: &project::Env, binding: &binding::ResolvedBinding,
|
||||||
) -> Result<(SweepFamily, Vec<usize>), BindError> {
|
) -> Result<(SweepFamily, Vec<usize>), BindError> {
|
||||||
let reload = |d: &str| {
|
let reload = |d: &str| {
|
||||||
blueprint_from_json(d, &|t| env.resolve(t))
|
blueprint_from_json(d, &|t| env.resolve(t))
|
||||||
@@ -1587,7 +1634,7 @@ fn blueprint_sweep_over(
|
|||||||
binder.sweep_with_lattice(|point| {
|
binder.sweep_with_lattice(|point| {
|
||||||
let sources = data.windowed_sources(from, to, env);
|
let sources = data.windowed_sources(from, to, env);
|
||||||
let window = window_of(&sources).expect("non-empty in-sample window");
|
let window = window_of(&sources).expect("non-empty in-sample window");
|
||||||
run_blueprint_member(reload(doc), point, &space, sources, window, 0, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K })
|
run_blueprint_member(reload(doc), point, &space, sources, window, 0, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, binding)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1598,14 +1645,14 @@ fn blueprint_sweep_over(
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn run_oos_blueprint(
|
fn run_oos_blueprint(
|
||||||
doc: &str, params: &[Cell], space: &[ParamSpec], from: Timestamp, to: Timestamp,
|
doc: &str, params: &[Cell], space: &[ParamSpec], from: Timestamp, to: Timestamp,
|
||||||
topo: &str, data: &DataSource, env: &project::Env,
|
topo: &str, data: &DataSource, env: &project::Env, binding: &binding::ResolvedBinding,
|
||||||
) -> (Vec<(Timestamp, f64)>, RunReport) {
|
) -> (Vec<(Timestamp, f64)>, RunReport) {
|
||||||
let reload = blueprint_from_json(doc, &|t| env.resolve(t))
|
let reload = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
|
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
|
||||||
let pip = data.pip_size();
|
let pip = data.pip_size();
|
||||||
let sources = data.windowed_sources(from, to, env);
|
let sources = data.windowed_sources(from, to, env);
|
||||||
let window = window_of(&sources).expect("non-empty out-of-sample window");
|
let window = window_of(&sources).expect("non-empty out-of-sample window");
|
||||||
let report = run_blueprint_member(reload, params, space, sources, window, 0, pip, topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K });
|
let report = run_blueprint_member(reload, params, space, sources, window, 0, pip, topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, binding);
|
||||||
(Vec::new(), report)
|
(Vec::new(), report)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1628,8 +1675,16 @@ fn blueprint_walkforward_family(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let space = blueprint_axis_probe(doc, env).param_space();
|
let space = blueprint_axis_probe(doc, env).param_space();
|
||||||
let topo = topology_hash(&blueprint_from_json(doc, &|t| env.resolve(t))
|
let probe_signal = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible"));
|
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
|
||||||
|
let topo = topology_hash(&probe_signal);
|
||||||
|
// Strict binding resolution, once per family; refusal is the established
|
||||||
|
// `aura: ` + exit-1 register (the roller's usage refusals stay exit 2).
|
||||||
|
let binding = binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())
|
||||||
|
.unwrap_or_else(|m| {
|
||||||
|
eprintln!("aura: {m}");
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
// Validate the `--axis` grid ONCE at the dispatch boundary, mirroring `aura sweep`
|
// Validate the `--axis` grid ONCE at the dispatch boundary, mirroring `aura sweep`
|
||||||
// (which resolves its axes a single time before any member runs). `walk_forward` fans
|
// (which resolves its axes a single time before any member runs). `walk_forward` fans
|
||||||
// the per-window closure out across the windows in parallel, so a `BindError` raised
|
// the per-window closure out across the windows in parallel, so a `BindError` raised
|
||||||
@@ -1642,19 +1697,19 @@ fn blueprint_walkforward_family(
|
|||||||
.next()
|
.next()
|
||||||
.expect("roller yields >= 1 window (validated above)")
|
.expect("roller yields >= 1 window (validated above)")
|
||||||
.is;
|
.is;
|
||||||
if let Err(e) = blueprint_sweep_over(doc, axes, first_is.0, first_is.1, data, env) {
|
if let Err(e) = blueprint_sweep_over(doc, axes, first_is.0, first_is.1, data, env, &binding) {
|
||||||
eprintln!("aura: {e:?}");
|
eprintln!("aura: {e:?}");
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
}
|
}
|
||||||
walk_forward(roller, space.clone(), |w: WindowBounds| {
|
walk_forward(roller, space.clone(), |w: WindowBounds| {
|
||||||
let (is_family, lattice) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env)
|
let (is_family, lattice) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env, &binding)
|
||||||
.expect("axes validated in the dispatch-boundary pre-flight");
|
.expect("axes validated in the dispatch-boundary pre-flight");
|
||||||
let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, Some(&lattice)) {
|
let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, Some(&lattice)) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
|
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
|
||||||
};
|
};
|
||||||
let (oos_equity, mut oos_report) =
|
let (oos_equity, mut oos_report) =
|
||||||
run_oos_blueprint(doc, &best.params, &space, w.oos.0, w.oos.1, &topo, data, env);
|
run_oos_blueprint(doc, &best.params, &space, w.oos.0, w.oos.1, &topo, data, env, &binding);
|
||||||
oos_report.manifest.selection = Some(selection);
|
oos_report.manifest.selection = Some(selection);
|
||||||
WindowRun { chosen_params: best.params, oos_equity, oos_report }
|
WindowRun { chosen_params: best.params, oos_equity, oos_report }
|
||||||
})
|
})
|
||||||
@@ -1686,7 +1741,11 @@ fn blueprint_mc_family(
|
|||||||
blueprint_from_json(d, &|t| env.resolve(t))
|
blueprint_from_json(d, &|t| env.resolve(t))
|
||||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
|
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
|
||||||
};
|
};
|
||||||
let topo = topology_hash(&reload(doc));
|
let probe_signal = reload(doc);
|
||||||
|
let topo = topology_hash(&probe_signal);
|
||||||
|
// Strict binding resolution (name defaults — mc's synthetic family binds
|
||||||
|
// no campaign overrides); the exit-free Err contract of this builder.
|
||||||
|
let binding = binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())?;
|
||||||
let pip = data.pip_size();
|
let pip = data.pip_size();
|
||||||
// probe the wrapped param_space (the same probe the sweep resolves against);
|
// probe the wrapped param_space (the same probe the sweep resolves against);
|
||||||
// MC needs it empty. `blueprint_axis_probe` is the single source of that wrap.
|
// MC needs it empty. `blueprint_axis_probe` is the single source of that wrap.
|
||||||
@@ -1711,7 +1770,7 @@ fn blueprint_mc_family(
|
|||||||
let family = monte_carlo(&base_point, &seeds, |seed, _base| {
|
let family = monte_carlo(&base_point, &seeds, |seed, _base| {
|
||||||
let sources = synthetic_walk_sources(seed);
|
let sources = synthetic_walk_sources(seed);
|
||||||
let window = window_of(&sources).expect("non-empty synthetic walk");
|
let window = window_of(&sources).expect("non-empty synthetic walk");
|
||||||
run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K })
|
run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, &binding)
|
||||||
});
|
});
|
||||||
// Silent-vacuous MC guard (refuse-don't-guess, C10): with >= 2 seeds, if every draw's
|
// Silent-vacuous MC guard (refuse-don't-guess, C10): with >= 2 seeds, if every draw's
|
||||||
// metrics are bit-identical to the first, no seed reached a distinguishable realization —
|
// metrics are bit-identical to the first, no seed reached a distinguishable realization —
|
||||||
@@ -3414,8 +3473,11 @@ mod tests {
|
|||||||
let (tx_ex, rx_ex) = mpsc::channel();
|
let (tx_ex, rx_ex) = mpsc::channel();
|
||||||
let (tx_r, _rx_r) = mpsc::channel();
|
let (tx_r, _rx_r) = mpsc::channel();
|
||||||
let (tx_req, _rx_req) = mpsc::channel();
|
let (tx_req, _rx_req) = mpsc::channel();
|
||||||
|
let signal = r_meanrev_signal(Some(3), Some(0.0));
|
||||||
|
let binding = binding::resolve_binding(signal.name(), signal.input_roles(), &BTreeMap::new())
|
||||||
|
.expect("the price role resolves");
|
||||||
let flat = wrap_r(
|
let flat = wrap_r(
|
||||||
r_meanrev_signal(Some(3), Some(0.0)),
|
signal,
|
||||||
tx_eq,
|
tx_eq,
|
||||||
tx_ex,
|
tx_ex,
|
||||||
tx_r,
|
tx_r,
|
||||||
@@ -3423,6 +3485,7 @@ mod tests {
|
|||||||
StopRule::Vol { length: 3, k: 2.0 },
|
StopRule::Vol { length: 3, k: 2.0 },
|
||||||
false,
|
false,
|
||||||
SYNTHETIC_PIP_SIZE,
|
SYNTHETIC_PIP_SIZE,
|
||||||
|
&binding,
|
||||||
)
|
)
|
||||||
.compile_with_params(&[])
|
.compile_with_params(&[])
|
||||||
.expect("r-meanrev signal wraps to a valid harness");
|
.expect("r-meanrev signal wraps to a valid harness");
|
||||||
@@ -3479,11 +3542,14 @@ mod tests {
|
|||||||
// from the synthetic 0.0001 — i.e. the real-data condition.
|
// from the synthetic 0.0001 — i.e. the real-data condition.
|
||||||
let pip_a = 1.0_f64;
|
let pip_a = 1.0_f64;
|
||||||
let pip_b = 0.1_f64;
|
let pip_b = 0.1_f64;
|
||||||
|
let signal_a = r_meanrev_signal(Some(3), Some(0.0));
|
||||||
|
let binding = binding::resolve_binding(signal_a.name(), signal_a.input_roles(), &BTreeMap::new())
|
||||||
|
.expect("the price role resolves");
|
||||||
let report_a = run_blueprint_member(
|
let report_a = run_blueprint_member(
|
||||||
r_meanrev_signal(Some(3), Some(0.0)), &[], &space, make_source(), window, 0, pip_a, "topo", &env, stop,
|
signal_a, &[], &space, make_source(), window, 0, pip_a, "topo", &env, stop, &binding,
|
||||||
);
|
);
|
||||||
let report_b = run_blueprint_member(
|
let report_b = run_blueprint_member(
|
||||||
r_meanrev_signal(Some(3), Some(0.0)), &[], &space, make_source(), window, 0, pip_b, "topo", &env, stop,
|
r_meanrev_signal(Some(3), Some(0.0)), &[], &space, make_source(), window, 0, pip_b, "topo", &env, stop, &binding,
|
||||||
);
|
);
|
||||||
// Guard: the run must have actually traded, else the invariant is vacuous.
|
// Guard: the run must have actually traded, else the invariant is vacuous.
|
||||||
assert!(
|
assert!(
|
||||||
@@ -4062,6 +4128,8 @@ mod tests {
|
|||||||
// Mint one member under a NON-default vol-stop regime (default is length=3, k=2.0).
|
// Mint one member under a NON-default vol-stop regime (default is length=3, k=2.0).
|
||||||
// run_blueprint_member stamps stop_length=8/stop_k=4.0 into the manifest params.
|
// run_blueprint_member stamps stop_length=8/stop_k=4.0 into the manifest params.
|
||||||
let non_default = StopRule::Vol { length: 8, k: 4.0 };
|
let non_default = StopRule::Vol { length: 8, k: 4.0 };
|
||||||
|
let binding = binding::resolve_binding("stoprepro", reload().input_roles(), &BTreeMap::new())
|
||||||
|
.expect("the price role resolves");
|
||||||
let report = run_blueprint_member(
|
let report = run_blueprint_member(
|
||||||
reload(),
|
reload(),
|
||||||
&[],
|
&[],
|
||||||
@@ -4073,6 +4141,7 @@ mod tests {
|
|||||||
&topo,
|
&topo,
|
||||||
&env,
|
&env,
|
||||||
non_default,
|
non_default,
|
||||||
|
&binding,
|
||||||
);
|
);
|
||||||
|
|
||||||
// persist exactly as the sweep/campaign paths do: store the blueprint, append the family.
|
// persist exactly as the sweep/campaign paths do: store the blueprint, append the family.
|
||||||
|
|||||||
Reference in New Issue
Block a user