Persist per-member trace streams for family runs (sweep/MC/walk-forward), gated by --trace #104
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?
Gap (validated)
Single backtest runs persist their drained streams to disk and are chartable:
aura run --trace <name>callspersist_traces(crates/aura-cli/src/main.rs:161),gated on the
--traceopt-in (:249,:320,:921), writingruns/traces/<name>/{index.json,equity.json,exposure.json}viaTraceStore(
crates/aura-registry/src/trace_store.rs:40).Family runs do not.
aura sweep|mc|walkforwardcallappend_familywithmetric-only
RunReports and persist nothing to the trace store(
run_sweep:500,run_walkforward:522,run_mc:721). Each family member doesrun a full harness with equity+exposure recorders and is drained inside its
per-member closure — but the raw rows are immediately folded to
metrics: summarize(...)and discarded (sweep_family:468-469,mc_family:686-687,run_oos:609-610).SweepFamily/McFamily/WalkForwardResultcarry onlyRunReports (manifest + metrics), never the streams.Consequence: the family-comparison view — the C21 differentiator (families of
harnesses: sweep / MC / walk-forward) — has no per-member trace data to chart.
The streams that would feed it exist transiently and are thrown away.
Goal
Each family member's equity + exposure streams land on disk, mirroring
single-run persistence, so (a) any single member is chartable today with the
existing viewer, and (b) the later family-comparison view has data to render.
Scope — the write side only
--trace <name>onaura sweep|mc|walkforward, mirroringaura run --trace. Without it, behaviour is byte-unchanged (no member tracedirs written — the current cardinality cost stays user-chosen, never imposed).
runs/traces/<name>/<member_key>/via the existingpersist_traces/TraceStore. The engine stays untouched: the per-member closures already holdthe drained rows (
rx_eq.try_iter().collect()); they bind those rows once and,when
--traceis active, callpersist_traces(&member_name, ...)beforefolding to metrics.
member_keyper family kind (deterministic, derivable from what eachfamily already keys its output lines by):
m00,m01, …),seed1,seed2, …),w00,w01, …).TraceStoreresolves
<name>/<member_key>as a subpath, the existingaura chart <name>/<member_key>charts any one member as-is.Out of scope (explicit — deferred to their own cycles)
of N members) — a genuine UI design fork, not settled here.
latent constant-factor concern; out of scope until cardinality is real).
Acceptance shape
aura sweep --trace swritesruns/traces/s/m00/,…/m01/, … each a completerun-dir (
index.json+equity.json+exposure.json) whose SoA contentround-trips through
TraceStore::read.aura mc --trace mwrites one dir per seed;aura walkforward --trace wwritesone dir per OOS window.
--trace→ no trace dirs created; existing family stdout/registry outputbyte-unchanged.
the same disjoint per-member sims, not a change to what they compute.
Invariants touched
C6 (the recorded stream is the replay source of truth), C7 (SoA scalar taps),
C8 (sinks/recorders), C18 (run registry) and C21 (families) — all upheld; this
adds a persistence side-effect at the family member boundary, mirroring the
single-run path, and changes no computation.
Design record (specify, Step 1.5 — derived fork decisions)
Spec:
0058-family-trace-persistence. Boss-mode autonomous run; the forksbelow were derivable (sources / code / existing-design consistency / risk),
so they are decided here, not bounced. Each is labelled derived with its
rationale, per the bold-decide stance.
Fork A — how member traces get retained: persist-in-closure vs. engine
carries the traces → persist inside the per-member closure (aura-cli),
gated by the active
--tracename; the engine stays untouched.Basis (derived): the three closures already live in aura-cli and already
drain the rows (
rx_eq.try_iter().collect()atsweep_family:468,mc_family:686,run_oos:609).persist_tracesis, by its own doc,engine-untouched
TraceStorewiring. Threading traces throughSweepFamily/McFamily/WalkForwardResult+ the engine'ssweep/monte_carlo/walk_forwardhigher-order fns would enlarge the blastradius across aura-engine for no gain. C1 computation stays pure; persistence
is a sink side-effect, exactly as in single runs.
Fork B — opt-in vs. always-on → opt-in via a new
--trace <name>onaura sweep|mc|walkforward, mirroringaura run --trace. Without it,behaviour is byte-unchanged.
Basis (derived): consistency with the single-run gate (
:249/:320/:921)user-chosen action, never imposed.
Fork C — member-key scheme + on-disk layout → each member persisted as a
standalone run-dir at
runs/traces/<name>/<member_key>/, keyedsweep =
m{NN}(odometer index), Monte-Carlo =seed{N}(draw seed),walk-forward =
w{NN}(OOS window index).Basis (derived): each family already identifies its members by exactly these
in stdout (
mcprints"seed": draw.seed; sweep / walk-forward print perpoint / per window in order).
TraceStoreresolves<name>/<member_key>asa subpath, so the existing single-run viewer charts a member with no
view-side code.
Fork D — view scope → the family-comparison VIEW (overlay /
small-multiples / rank-top-N of N members) is out of scope, its own later
cycle.
Basis (derived): it is a genuine UI design fork (how to render N members) with
real trade-offs the write side does not settle; deferring keeps this cycle
bounded and single-plan. A scope boundary, not a guess at UI taste.
Correction to Fork C (member-key scheme) — forced by C1 parallelism
Grounding the closure signatures revealed the engine higher-order fns are
F: Fn(...) -> RunReport + Sync(sweep.rs:314,mc.rs:124,walkforward.rs:187): family members run in parallel across threads (C1parallelism across sims). A runtime ordinal counter (
m00,m01, … assignedas the closure fires) would therefore be non-deterministic — the
member→index mapping would depend on thread scheduling, a C1 violation. The
final
family.pointsvector is in deterministic odometer order, but the closuredoes not receive its odometer position, and persisting after the sweep would
require the engine to carry the traces (the rejected alternative of Fork A).
Revised decision (derived): member keys are content-derived and
deterministic, computed from what each closure already receives:
seed{N}(the draw seed, passed to the closure).zip_params(&space, point)(a stable, collision-free encoding of the gridpoint — distinct points differ in ≥1 swept axis).
index, which uniquely identify the window).
Basis (derived): preserves Fork A (engine untouched, persist-in-closure) and
C1-determinism under the parallel
Fn + Syncmodel, at the cost of prettyordinal keys. Content-derived keys are in fact more informative (self-describing)
and are guaranteed deterministic + collision-free, which a runtime counter is
not. Concurrent
TraceStore::writeto disjoint member dirs needs no lock(disjoint paths;
create_dir_allis idempotent) — consistent with C1's"concurrently executable without locking".