Orchestrate harness families from a loaded blueprint (sweep / MC / walk-forward) #166
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Cycle 2 of the World/C21 milestone: the World orchestrates families of harnesses built from a loaded blueprint — the meta-level differentiator (C21: families, not the single backtest).
Deliverable
sweep / Monte-Carlo / walk-forward orchestration that accepts a loaded blueprint (the cycle-1
aura runload path) plus the family's axes, builds one harness instance per family member from the blueprint-data, runs them (C1: members are disjoint → parallel), and aggregates to the sameFamilyRunRecordshape the hard-wired*_sweep_familypaths produce today.Acceptance (to be sharpened at spec time)
FamilyRunRecordbyte-identical to the equivalent hard-wired-path family run (same members, same aggregation).families.jsonl) unchanged.Notes
Depends on the cycle-1 run-from-data load path. Part of the World/C21 milestone.
Derived design decisions (cycle-2 grounding, specify Step 1.5)
A read-only recon confirmed cycle 2 (families from a loaded blueprint) is a
DERIVABLE extension of the existing sweep machinery — no design fork. The
member-construction (
wrap_stage1r(loaded_signal)→param_space()→SweepBinder.axis(name, values).sweep(|point| bootstrap_with_cells(point))) andthe family store (
append_family) are blueprint-agnostic; only the signal sourceand the axis-naming surface change.
Decisions:
aura sweep <blueprint.json>; Monte-Carlo and walk-forward over a loaded blueprint are thesame machinery repeated per verb and follow as a fast follow-on (their own
small cycle), so cycle 2 stays tractable.
--axis <name>=<csv>. A loaded signal is arbitrary (not onlystage1-r's
fast/slow), so axes are user-supplied by name, resolvedagainst the loaded blueprint's
param_space()(the existingresolve_axesby-name binding). The hard-wired
--strategy stage1-r --fast/--slowpath staysuntouched (#159). The
.json-file discriminator mirrors cycle 1'saura run <blueprint.json>arm.topology_hash(&loaded_signal)— all members of a sweep over one signal shareit (same topology, only params vary;
member_keydistinguishes members). Thiskeeps a generated member reproducible (C18/C24), the natural consequence of
topology-as-data. (Hard-wired members keep
None— their topology is baked,identified by
commit.)FamilyKind::Sweep+append_family→families.jsonl,byte-identical shape to the hard-wired sweep.
Ready for spec production.
Grounding refinements from plan-recon (derived decisions)
The recon confirmed the mirror is a clean reuse but surfaced five design points;
all derivable, resolved here.
Q1 (load-bearing) —
Compositeis!Clone, so the signal is re-loaded permember.
Compositecannot deriveClone(itsPrimitiveBuilders hold abuild: Box<dyn Fn>closure — the same reason it carries noPartialEq/Debug,#164), and
bootstrap_with_cells(self, point)consumes the graph. The hard-wiredstage1_r_sweep_familygets a fresh per-member graph for free by re-callingstage1_r_graph(...)inside its.sweepclosure. Decision:run_blueprint_sweeptakes the serialized doc (&str,Send+Sync) andre-loads a fresh signal via
blueprint_from_json(doc, &|t| std_vocabulary(t))inside the closure, per member — cheap relative to a backtest.
topology_hashiscomputed once from the doc.
Q2 —
stop_open=false,reduce=true,cost=None. The loaded signal is aprice→biasleg with NO stop knobs; the vol-stop lives inwrap_stage1r'sscaffolding, bound to the R-defining
STAGE1_R_STOP_*constants (stop_open=false).The user sweeps the signal's own open knobs (
--axis fast.length=…).reduce=true(folded, mirroring
reduce = trace.is_none());cost=None. (Sweeping the stop wouldneed
stop_open=true; the spec shows only signal axes, so it stays bound.)Q3 — the family-equivalence test compares each member to
run_signal_stage1rper point, NOT to the hard-wired sweep. The hard-wired
stage1_r_sweep_familyruns
stop_open=true(the open-stop scaffolding); a blueprint sweep runsstop_open=false. The clean, exact equivalence is: eachrun_blueprint_sweepmember ==
run_signal_stage1r(signal, &point_params, …)(cycle 1's single run —the SAME
stop_open=falsescaffolding), trivially identical. This is "the loadedpath agrees with the Rust path", the honest acceptance.
Q4 — parser stays pure →
DataChoice; the arm converts.parse_sweep_blueprint_argsreturns aDataChoice(via the existingRealWindowGrammaraccumulator), and the dispatch arm converts withDataSource::from_choicebefore callingrun_blueprint_sweep(… data: DataSource)— mirroring the existing sweep arm.
Q5 —
--axis <name>=<csv>lexes each token to aScalarby shape. A by-nameaxis has no statically-known kind at parse time. Lex: integer-shaped token →
Scalar::i64, elseScalar::f64(try-i64-then-f64).resolve_axesthenkind-checks each value against the slot's declared
ParamSpec.kind; a mismatch isa clean named error (the
KindMismatcharm), an unknown axis name a cleanUnknownKnob— neither panics.Investigated: reduce vs full R-equivalence at window-end — no divergence
A follow-up flagged that the reduce-mode sweep path (
GatedRecorder) might report adifferent R from a full single run (
Recorder) for a position left open at the window'send (
n_open_at_end). Investigated — there is no divergence; the equivalence holds forevery window-end shape, and it is pinned by three currently-green tests:
gated_recorder_then_summarize_r_equals_raw_summarize_r(crates/aura-engine/tests/streaming_reduction_equivalence.rs) — unit, drives a record
that ends in a non-gated open-at-end row; asserts
summarize_r(gated) == summarize_r(full),n_open_at_end == 1included.stage1_r_single_run_output_golden(crates/aura-cli/tests/cli_run.rs) — pinsn_open_at_end:1for the default single run at fast=2/slow=4 (so that param isopen-at-end, not a closed-flat ending).
blueprint_sweep_member_equals_single_run_and_shares_topology_hash(crates/aura-cli/src/main.rs) — integration: a reduce-mode sweep member's full
metricsequals a full-recorder single run at fast=2/slow=4 (the open-at-end param above).
Mechanism:
GatedRecorder.finalize(crates/aura-std/src/gated_recorder.rs) emits the truefinal row exactly once when it was not already emitted as a gated (closed) row, and
summarize_rdecidesn_open_at_endsolely from the last row'sOPENflag — so bothpaths present the same final row. The harness calls
finalizeon every node after thestream drains (crates/aura-engine/src/harness.rs), and both run paths share that
machinery.
The earlier hedge ("chosen params avoid that") was inaccurate: fast=2/slow=4 is itself an
open-at-end param and the equivalence holds there. The one shape not yet pinned by a
direct integration test — a full
aura runvsaura sweepmember where the final bar isa non-gated open hold (CLOSED=false, OPEN=true) through the real run loop — is covered by
composition (finalize-is-called + the unit equivalence) and will be pinned directly by the
#158 reproduce coverage (a sweep member must re-derive a standalone run bit-identically),
scoped to include an open-at-end param.