114fa3d718fe18bcf428fcd0d6900e6ddd916852
107 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d925a747cb |
feat(0086): cost-flag CLI ergonomics — units note, named diagnostics, non-R-harness guard
#153 (friction surfaced by the milestone-#148 close fieldtest). Three cost-flag fixes on `aura run`, all on the pure parse_run_args grammar: - Units discoverable: a COST_FLAGS_NOTE appended to the run usage + --help (price units, charged in R as cost/|entry-stop|, per-ENGINE-cycle carry, >= 0, stage1-r only). - A negative rate names the cause + flag ("<flag> must be non-negative, got <v>") via a factored parse_nonneg_rate helper, not a bare usage dump. - A cost flag on a non-R harness (sma/macd) is refused (exit 2, "cost flags require an R-evaluator harness (stage1-r)") instead of silently ignored — the named spec_gap, decided reject/fail-loud (refuse-don't-guess, recorded on #153), pinned by a new C10 ledger note. Internal RunArgs/RunData/HarnessKind gain #[derive(Debug)] (behaviour-neutral) so the pure parse_run_args unit tests can use expect_err. Verified independently: cargo test -p aura-cli green, the eight stage1-r cost goldens byte-unchanged; clippy -p aura-cli --all-targets -D warnings clean. Only the grammar/usage strings and the previously-unguarded sma-cost path changed; the cost-model graph and R math are untouched. closes #153 |
||
|
|
f5e00a9c72 |
feat(0085): per-cycle-held accrual — CarryCost + ChargeMode (approach B)
Cycle 5 of milestone #148 (cost-model graph in R): the per-cycle-held accrual mechanism — C10's "per-cycle-held factors accrue over the hold". A carry/funding cost is charged for every cycle a position is held, so the net_r_equity equity curve bleeds continuously over the hold (approach B, "ja, mach B") rather than stepping at close. Mechanism (logged on #148): ChargeMode { AtClose, PerHeldCycle } is a property of the cost factor, read by the one shared CostRunner (no second runner type). The AtClose arm is the pre-cycle-5 eval tokens VERBATIM (IEEE-754 byte-identity). The PerHeldCycle arm accrues `per` into `acc` each held cycle, dumps the accrued total into `cum` at close (resetting acc), and marks the open position via a growing open_cost_in_r. The bleed lives in open_cost_in_r, which the net_r_equity tap already subtracts — so summarize_r and the CLI net_eq wiring are UNCHANGED, and the cycle-0083/0084 net_expectancy_r goldens (-614.3134020253314 flat, -615.0304388396047 composed) + the C18 no-cost golden stay byte-identical. CarryCost is a ConstantCost twin differing only in charge_mode() -> PerHeldCycle (a labelled stress parameter, the flat base of the accrual family). Wired through the existing cost_graph/CostSum aggregation; a per-trade ConstantCost and a per-held-cycle CarryCost compose in one run (the costs sum per-field — the composed golden is exactly the sum of the two single-cost charges). New run-path --carry-per-cycle flag (sweep/walkforward/mc pass None, as the existing cost flags do). Tests: 2 RED-first unit B-proofs (open_cost grows over the hold, dumps the total at close, acc resets between trades — the discriminator a scalar net_expectancy_r cannot see); the CarryCost twin's 5 unit tests; 2 captured net_expectancy_r goldens (-768.2095026600887 carry, -1383.7939051991182 composed); a net_r_equity-bleeds-over- the-hold integration test (the terminal open hold's r_equity-net_r_equity gap strictly grows); plus negative-rate-refused-exit-2, monotone-in-rate, and zero-rate-exactly-free guards. Verified: cargo test --workspace (0 failures), cargo build --workspace, cargo clippy --workspace --all-targets -- -D warnings clean. summarize_r / aura-analysis untouched. Deferred to later #148 cycles (logged there): notional-based carry, the calendar-aware overnight swap, sweep-path cost. refs #148 |
||
|
|
face1879a6 |
audit(0084): cycle close — drift-clean (code); C10 cycle-0084 note + doc fixes
Cycle 4 of milestone #148 (cost-graph composite-builder). Architect drift review over fc52b4f..d5c44dd: drift_found, every item dispositioned. What holds (architect, evidence-of-review): C16/C9 — cost_graph lives in aura-composites, the factors in aura-std, aura-engine stays domain-free; the composite is ordinary downstream nodes. C11 behaviour-preservation is structural, not just asserted — the composite inlines to the same flat fan-in as the deleted CLI block (identical node set/order, cost[k].field slot map, geometry fan, net_eq terms/signs, cost_rec cols), so the cycle-0083 net_expectancy_r goldens stay byte-identical. C23 — role/port names non-load-bearing; 4 fixtures pin the role-set + output triple. Resolved this commit: - [high] ledger C10: the cycle-0083 note's "Still deferred (decision E)" is removed (E shipped this cycle) and a cycle-0084 realization note added. - [medium] cost_graph .leak() provenance (aura-composites/src/lib.rs): the "risk_executor precedent" was imprecise — that .leak() lives in risk_executor's TEST (one-shot, leak-safe), not its production builder. Doc corrected: the leak is fine for one-shot run-path construction but must be interned (the COL_PORTS production pattern) before cost reaches the sweep path. - [low] cli_run.rs golden docstring cited the deleted `slot * COST_WIDTH + f` CLI wiring; updated to the cost_graph composite's per-node cost[k].<port> wiring. Deferred (tracked, not pending) -> #152: - [medium] the cost[k].<port> index-namespacing restated across CostSum / cost_graph / the CLI (a build-validated lockstep — a divergent name fails loudly at g.build(), NOT the silent positional kind 0083 collapsed), AND the per-build .leak() that becomes an allocation regression on the per-member sweep path. Both are harmless on the current run-only cost path; the proper fix is one sweep-safe single-source cost-port-name contract (interned), best built with the deferred sweep-cost cycle. Regression gate: cargo test --workspace (0 failures), clippy --workspace --all-targets -- -D warnings clean, cargo doc clean — the architect is the primary gate (no dedicated regression script). Ephemera (spec 0084 + plan 0084) git rm'd. Milestone "Cost-model graph (in R)" remains OPEN. refs #148 |
||
|
|
d5c44dd1ea |
feat(0084): cost-graph composite-builder
Cycle 4 of milestone #148 (Cost-model graph in R), decision E. New `cost_graph(Vec<PrimitiveBuilder>) -> Composite` in aura-composites: it fans the 4 PM-geometry inputs to N cost nodes, surfaces each node's extra inputs (discovered via schema introspection past GEOMETRY_WIDTH) as `cost[k].<port>` roles, sums them through CostSum, and exposes the 3-field aggregate. Replaces the CLI's manual slot-indexed cost-wiring + the hardcoded MAX_RUN_COST_NODES=2 cap with one principled composite of arbitrary arity. GEOMETRY_WIDTH re-exported from aura-std (first cross-crate consumer). Runtime port names .leak()'d (the risk_executor precedent), a bounded one-time cost at blueprint construction. Behaviour-preserving at the value level: the composite inlines at bootstrap (C11) to the same flat fan-in, so the two cycle-3 net_expectancy_r goldens (-614.3134020253314 flat, -615.0304388396047 composed), the C18 no-cost golden, and the full suite stay green verbatim. Honours C9 (a composite of cost nodes is ordinary downstream nodes), C16 (wiring lives in aura-composites, not aura-engine), C23 (label drift under cost_graph nesting is permitted; no with-cost graph label/shape golden exists). Four aura-composites unit tests pin the exposed contract: the two planned (n=2 heterogeneous role set; n=1 no-extra shape) plus two the implementer added that strengthen the headline — arbitrary-arity per-node namespacing (n=3, two VolSlippageCost -> distinct cost[1]/cost[2].volatility, the property that retires the 2-node cap) and geometry fan-once (n=2 extra-free -> only the 4 geometry roles). Accepted nit (held, non-gating): the CLI reconstructs the `cost[k].volatility` role name to feed the vol node's extra input — a string coupling to the composite's public role-naming convention. Kept: role-based composite wiring addresses roles by name (as the CLI already does for "closed"/"bias"/"price"); the convention is tested + build-validated; a decoupling accessor would widen this cycle's scope. Verified: cargo test --workspace (0 failures), clippy --workspace --all-targets -D warnings clean, cargo doc -p aura-composites clean. refs #148 |
||
|
|
6b53c239dd |
feat(0083): CostNode trait + shared cost-record contract
Lift the duplicated cost-node skeleton — shared verbatim by ConstantCost and VolSlippageCost and locked by convention against CostSum — into one abstraction: - A new aura-std/src/cost.rs owns the cost-record contract (COST_WIDTH, COST_FIELD_NAMES, GEOMETRY_WIDTH), the CostNode factor trait (one hook: cost_numerator, in price units), the generic CostRunner<F> adapter that holds the shared state (cum, out) and the co-temporality skeleton (geometry gating, numerator/latched R-normalization, closed/open charge, 3-field emit), and a cost_node_builder schema assembler. - ConstantCost and VolSlippageCost become thin CostNode factors; their new() returns CostRunner<Self>, so every existing call site and unit test binds transparently and stays green verbatim. - CostSum and main.rs drop their local triple consts for the shared COST_FIELD_NAMES — the cycle-2 audit-flagged by-convention 3-field lockstep is now a structural single-source contract (the four copies of the triple collapse to one). main.rs also reads COST_WIDTH in place of the literal slot stride. Behaviour-preserving: the builders emit unchanged schemas (same port names), so the wiring, the net_r_equity seam, and summarize_r are untouched; the numerator/latched token form is preserved verbatim (IEEE-754 byte-identity). The existing suite is the regression net — all green: the per-node unit tests pass verbatim (0.5/1.5, 0.375/1.375), the composition E2E exact, the C18 no-cost golden byte-identical. Two new CLI characterization goldens pin the exact net_expectancy_r of the flat and composed cost paths (the prior tests only asserted net < gross, which a numerator drift would pass silently). New cost.rs runner/contract tests + the HalfSpreadCost author doctest cover the new surface. Deviation from the plan: CostNode::name() was dropped (the plan over-specified it) — it is dead surface, nothing consumes it (CostRunner forwards label(); cost_node_builder takes the name as an explicit arg). Removed from the trait, both impls, the doctest, and the test stubs; build + suite + clippy green. Verified: cargo build --workspace --all-targets clean; cargo test --workspace 0 failures; cargo clippy --workspace --all-targets -- -D warnings clean; doctest green. refs #148 |
||
|
|
6c7f5dd147 |
audit(0082): cycle close — drift-clean; C10 cycle-0082 note + cost-flag validation symmetry
Cycle 0082 (cost-graph composition, cycle 2) closes drift-clean against C10. The
architect review confirms the cycle is fundamentally sound:
What holds: VolSlippageCost + CostSum are ordinary aura-std downstream nodes
(C9/C8), cost stays out of aura-engine; the vol proxy is a trailing realized range
(C2, no look-ahead); feed-forward, no equity feedback (C1); the co-temporality
contract (gate cost nodes on PM geometry only, 0-cost on a cold state input) is
consistent with C1/C2/C9 and the cycle-1 positional join, and charging 0 during
warm-up is honest (no estimate yet, not a fabricated guess). The no-cost C18 golden
is byte-identical.
Drift resolved at close:
- [medium -> fixed] docs/design/INDEX.md (C10): added the cycle-0082 realization
note. The load-bearing co-temporality contract (generalizes to all future cost
factors) lived only in code comments + spec 0082; the ledger is now its durable
home once the spec is removed.
- [low -> fixed] crates/aura-cli/src/main.rs: --cost-per-trade now rejects negatives
at parse, symmetric with --slip-vol-mult (it previously deferred to the node
assert's panic). Run-path tests + the no-cost golden stay green.
Carry-on (documented deferrals, not this cycle's debt to fix):
- The 3-field cost triple {cost_in_r, cum_cost_in_r, open_cost_in_r} is restated
by-convention across ConstantCost, VolSlippageCost, and CostSum (compiler-unlinked
lockstep) — to be unified by the general CostNode trait, now justified by two
concrete nodes (#148).
- SLIP_VOL_LENGTH=5 is sized to warm within the synthetic smoke fixture; documented
in the const doc (on real M1 data any reasonable window warms).
- A future per-cycle-held accrual (carry/funding) accrues over holding duration,
which will force a summarize_r fold change and stress the positional seam — flagged
for that deferred cycle.
Regression gate green and unchanged: full workspace suite 0-fail, clippy --workspace
--all-targets -D warnings clean. No baseline moved -> no ratify.
Spec + plan ephemera removed (git rm docs/specs/0082, docs/plans/0082).
refs #148
|
||
|
|
7f3756a395 |
feat(0082): cost-graph composition — VolSlippageCost + CostSum net-R aggregate
Cycle 2 of the "Cost-model graph (in R)" milestone (#148): the milestone's real architectural claim — the cost graph composes. Two cost nodes now sum into one net-R curve while summarize_r and the net_r_equity tap stay structurally unchanged. - VolSlippageCost (aura-std): a second, STATE-DEPENDENT cost node; per-trade charge = slip_vol_mult * volatility / |entry-stop|, in R. The vol is an independent short-horizon realized range (SLIP_VOL_LENGTH=5, distinct from the stop's EWMA-3) — scaling slippage by the stop's own vol would collapse cost-in-R to a constant (indistinguishable from ConstantCost). - CostSum (aura-std): the cost-graph OUTPUT node — sums N cost nodes' 3-field cost-in-R records per-field into one aggregate. summarize_r and net_r_equity read the aggregate (one home for cost; n=1 is the identity, so the cost path is uniform). A future CostNode trait (deferred) will unify the cost-triple the two producer nodes currently restate by-convention. - Run path: --cost-per-trade and --slip-vol-mult combine, their costs summing into the net-R curve; a hoisted vol proxy (RollingMax-RollingMin) keeps the single feed. Run-path-scoped; sweep/walkforward/mc pass None. Co-temporality contract (the load-bearing design decision; corrected from the signed spec after the implement-loop correctly BLOCKED Task 3 on it). summarize_r positional-joins cost[i] <-> record[i], so the cost stream must be co-temporal 1:1 with the PM record. A cost node is therefore gated ONLY by the PM geometry (closed/open/entry/stop); a not-yet-warm state input (the vol proxy warms later than PM) contributes 0 cost that cycle rather than withholding and desyncing the stream. This makes co-temporality structural + warmup-independent, preserves the C18 golden, and generalizes to any future cost factor. The rejected alternative (a key-join in summarize_r) would have moved the golden and pushed cost-graph logic into the post-run fold. Recorded on #148. Tests: VolSlippageCost + CostSum unit sets (incl. the co-temporal-zero-during- warmup case); the CLI composition run (both flags -> net_both < net_flat, net_r_equity persisted); node-level EXACT additive composition (net_both == net_flat + net_vol - gross over the real nodes), the aggregate net_r_equity == post-run net total, and CostSum(1) identity. Full workspace suite green; clippy -D warnings clean; the no-cost C18 golden byte-identical (the regression floor). Deferred (later cycles of this milestone): the general CostNode trait + cost-graph composite-builder (now justified by two concrete nodes), the conviction-weighting R-aggregation axis, and cost on the reduce-mode sweep path. Spec + plan amended to the corrected contract; both are cycle ephemera (git rm at cycle close). refs #148 |
||
|
|
3fe4684190 |
feat(0081): cost-model graph cycle 1 — ConstantCost node + net-R seam
Lands the first cycle of the "Cost-model graph (in R)" milestone: cost is
now a composable in-graph node, not a post-run scalar (C10).
- ConstantCost (aura-std): a flat round-trip cost per trade, emitted in R
as a 3-field record {cost_in_r, cum_cost_in_r, open_cost_in_r} isomorphic
to PositionManagement's R-triple. R-pure: cost_in_r = cost_per_trade /
|entry-stop|; notional cancels, no equity held.
- summarize_r folds the cost stream into net_expectancy_r, replacing its
scalar round_trip_cost (one home for cost, no double-count). Positional
co-temporal join; an empty stream is the gross-R baseline.
- net_r_equity tap (stage1_r_graph): LinComb(4) over [cum_realized_r,
unrealized_r, -cum_cost_in_r, -open_cost_in_r] -> Recorder, a sibling of
r_equity; --cost-per-trade on the run path wires the node + tap + fold.
- A no-cost run is byte-identical: no net_r_equity tap, net == gross, the
C18 golden unchanged.
Tests: exact-subsumption (the node-stream net == mean(r_i - cost_i) over
ALL trades incl. the window-end open one), the in-graph net_r_equity tap +
net < gross E2E, the no-cost golden held. Full suite green; clippy clean.
Implement-time decisions ratified (recorded on #148): the cost stream is
recorded 3-wide (the node's full output; summarize_r reads col 0/2, col 1
feeds the in-graph net curve) — resolving a 2-wide/3-wide inconsistency in
the plan's Step 3; Trade drops its `latched` field (cost now arrives in R),
so r_col ENTRY_PRICE/STOP_PRICE become #[allow(dead_code)] layout contract.
Scoped to the run path (non-reduce); sweep/walkforward/mc pass None — cost
on the reduce-mode sweep path and OOS-pooling cost are a deferred follow-up.
Known pre-existing concern (not this cycle): aura-std lib.rs module doc
still names "realistic broker profiles", retired by the C10 rework.
refs #148
|
||
|
|
3e8fd73f68 |
tidy: clear the milestone fieldtest findings (refusal reasons, --help, ledger + glossary)
Resolves the four non-bug findings from the inferential-validation milestone
fieldtest (the bug — the broken-pipe panic — is fixed in
|
||
|
|
1088320571 |
fix: terminate cleanly on a broken stdout pipe across all family-emitting commands
GREEN for the broken-pipe RED test (
|
||
|
|
c750b9fbc3 |
test: RED — family-emitting CLI commands must survive an early-closing stdout reader
Captures the pre-existing broken-pipe panic surfaced by the milestone fieldtest ([bug] finding): every multi-line subcommand (sweep / mc / walkforward / runs family / generalize) panics "failed printing to stdout: Broken pipe (os error 32)" and exits 101 when the stdout reader closes early (| head, | less, a closed UI pane), instead of terminating cleanly. Root cause: Rust's runtime resets SIGPIPE to SIG_IGN at startup, so a write to a closed pipe returns EPIPE and println! panics rather than the process dying quietly. crates/aura-cli/tests/cli_broken_pipe.rs spawns `aura sweep`, closes the stdout read end mid-stream, and asserts no panic in stderr + exit != 101. Verified RED (reproduces 6/6). The GREEN fix follows in the next commit. refs #146 |
||
|
|
0a06afed29 |
feat(0078): cross-instrument generalization as a validation step
Last piece of the Inferential-validation milestone (#146): a new `aura generalize` subcommand grades how consistently a single candidate holds across a set of instruments — the across-instrument axis of the same anti-false-discovery concern as trials-deflation (#144, within one family) and plateau-over-peak (#145, within one instrument's surface). Aggregator/validator, not a third selector (user-ratified): it runs a brought candidate (a single-cell stage1-r grid — --fast/--slow/--stop- length/--stop-k pinned to single values) across an instrument list (--real SYM1,SYM2,...), collects per-instrument R-metrics, and reduces them to a worst-case floor (min over instruments) + a sign-agreement count + the per-instrument breakdown. R-only (C10 — R is the only account/instrument-agnostic unit); the worst-case min is the cross- instrument sibling of PlateauMode::Worst and satisfies the non- domination constraint by construction (a strong instrument cannot lift a min). Rejected: a cross-instrument t-stat mean/std*sqrt(M), which reintroduces the pooled mean the milestone warns against. - RunManifest gains a first-class `instrument` lineage field (serde- widened, C14/C18 — legacy lines load as None, existing manifest bytes unchanged); the compat read-mirror threads it in lockstep. - FamilyKind::CrossInstrument (C12 comparison axis); the M per-instrument runs persist as a CrossInstrument family, each member self-identifying via its stamped instrument. The aggregate is recomputable from the members (McAggregate precedent), printed live. - The generalization reduction lives registry-side beside optimize_ deflated/optimize_plateau (C9), reusing resolve_metric/metric_value/ is_r_metric; check_r_metric is the data-free pre-check the CLI runs before evaluating any instrument. - Additive: existing sweep/walkforward/mc/standalone paths stay byte- identical (C23 — instrument omitted when None via skip_serializing_if). Verified by the orchestrator: cargo test --workspace green (0 failed — the new generalization_*, check_r_metric, parse_generalize_*, and the gated cross-instrument E2E all pass; the E2E ran the real GER40/USDJPY path on this host, exit 0); cargo clippy --workspace --all-targets -D warnings clean. closes #146 |
||
|
|
cb32658fd1 |
feat(0077): prefer a robust parameter plateau over the in-sample peak
Add an opt-in plateau selection objective for walk-forward, recorded on the same RunManifest.selection carrier #144 introduced. Default argmax is byte-identical (C23); plateau is reached only via a new --select flag. What landed: - FamilySelection / SelectionMode reshape (report.rs): the selection RULE (mode) and its ANNOTATION are orthogonal — deflation fields become Option (present iff Argmax), plus PlateauMean/PlateauWorst variants and neighbourhood_score/n_neighbours (present iff Plateau*). Legacy lines still load: the deflation scalars deserialize from bare values via serde default (C14/C18). compat.rs embeds FamilySelection by value — reshape flows through untouched. - GridSpace::axis_lens() + SweepBinder::sweep_with_lattice (engine): the grid radixes in param_space()/odometer order. sweep() delegates to sweep_with_lattice with the lattice dropped, so every existing .sweep() caller is byte-unchanged. - optimize_plateau + PlateauMode + closed_neighbourhood (aura-registry, C9): each member scores as the mean/worst of its closed mixed-radix grid neighbourhood's metric_value; the winner is the smoothed argmax by the metric's direction (earliest-odometer tie, as optimize). Pure, no RNG (C1); in-sample only (C2). A higher_is_better helper now sources the per-metric direction once, shared by metric_cmp and optimize_plateau. - CLI --select <argmax|plateau:mean|plateau:worst> (default argmax; unknown token exits 2). walkforward_family dispatches via a select_winner helper; sweep_over/stage1_r_sweep_over carry the lattice as Option<Vec<usize>>. runs-family display gains a plateau(<mode>)=<score> over <n> cells line. RandomSpace-refuse: walkforward has no --random producer, so the plateau-without-lattice guard is structural — select_winner returns the refuse on a None lattice (the caller prints the message and exits 2), unit-tested at the helper, not via a --random E2E (decided + recorded on #145 comment 1974, with the lattice-seam fork). Tests: plateau-picks-plateau-not-spike, mean-vs-worst, lower-is-better direction-flip, closed-neighbourhood golden, single-member degeneracy, C1 determinism; CLI select-parse, select_winner refuse; E2E argmax-byte-identical (C23), plateau:mean and plateau:worst provenance. Full workspace suite green; clippy --all-targets -D warnings clean. closes #145 |
||
|
|
a2959050a4 |
feat(0076): deflate the sweep winner's metric for the number of trials
Adds optimize_deflated beside optimize in aura-registry: the in-sample walk-forward selection now records, on each OOS winner's manifest, how much the winner's metric is inflated by the size of the search it won — a deflated score and (R arm) an overfit probability — without changing which member wins (additive, C23; a regression test pins optimize_deflated's winner == optimize's). - R arm (sqn_normalized/expectancy_r/...): a centred moving-block reality-check. Each member's per-trade R series is mean-subtracted to impose the no-edge null, resampled via the r_bootstrap kernel (extracted to a shared resample_block, byte-identical), and the best-of-K null-max gives overfit_probability = (count(null >= raw)+1)/(n+1) and deflated_score = raw - p95(null). Deterministic given the seed (C1). - total_pips arm: a closed-form expected-max-of-K dispersion floor (inv_norm_cdf + expected_max_of_normals), no probability. The record (FamilySelection on RunManifest) rides the proven serde(default, skip_serializing_if) widening + a compat.rs mirror, so legacy runs.jsonl/families.jsonl lines load unchanged and unstamped sweep/mc/standalone manifests stay byte-identical (C14/C18). The SelectionMode enum reserves a Plateau slot for #145. CLI: walk-forward stamps both arms; runs family <id> rank surfaces a human-readable deflated/overfit line. Quality review (re-dispatched after the implement-loop's quality gate exhausted) caught a real defect: on the R arm with positive resamples but no member carrying trade_rs (a family loaded from disk — trade_rs is serde-skipped), the null was all-NEG_INFINITY, turning deflated_score into +inf. Fixed by collapsing "no usable null" (zero resamples OR no trade_rs) into one degenerate floor (deflated == raw, overfit == 1.0); pinned by optimize_deflated_no_trade_rs_floors_instead_of_infinity. Verified: cargo test --workspace and cargo clippy --workspace --all-targets -D warnings both green. Frictionless Stage-1 R; n_eff advisory and reload-time recompute deferred (spec 0076 Out of scope). closes #144 |
||
|
|
c192dfd344 |
audit(0075): cycle close — drift-clean; doc-link + trade_rs tripwire tidy; drop ephemera
Architect drift review (c99e7b8..2e77ab1) against the design ledger +
CLAUDE.md invariants: drift-clean, debt only — no contract or spec
violation. Verified intact: C18 wire shape (trade_rs is #[serde(skip)] +
excluded from the hand-written PartialEq; runs.jsonl/families.jsonl bytes
and digests unmoved; mc_r_bootstrap is a printed line, never a record),
C1 determinism (seeded r_bootstrap + disjoint walkforward windows, both
pinned by byte-identical-rerun e2e tests), C7 frictionless Stage-1
(round_trip_cost stays 0.0), C8/C12 streaming reduction (IS sweep folds
O(trades)/member, OOS run non-reduce). The verbatim summarize_r /
r_metrics_from_rs float duplication is guarded by the cross-reducer
equality test — sound.
Tidy applied this close:
- doc-link drift: the new E[R] notation in r_bootstrap / RBootstrap /
run_mc_r_bootstrap doc-comments was parsed by rustdoc as an intra-doc
link `[R]` (4 warnings). Wrapped in backticks; doc build now clean.
- coverage micro-gap [low, architect]: added a tripwire asserting a
populated trade_rs is absent from the serialized JSON (the serde(skip)
C18-preservation property had no direct test — only Vec::new() was
serialized before).
Carry-on (benign debt, not fixed): the mc usage literal carries a `usage:`
prefix the sibling sweep/walkforward usages omit — required because the
new `["mc", rest @ ..]` arm now handles the `--real` rejection and the
preserved golden asserts stderr.contains("usage"); churning the golden to
drop the prefix is not worth it.
Removed the cycle's ephemeral spec + plans (docs/specs/0075,
docs/plans/0075, docs/plans/0076) per the lifecycle rule.
Verified: cargo test --workspace 610 passed / 0 failed, clippy -D warnings
clean, cargo doc --no-deps clean.
closes #139
|
||
|
|
2e77ab1dda |
feat(0075): Monte-Carlo R-bootstrap for OOS validation (iter 2)
Add `aura mc --strategy stage1-r [--real <SYM>]`: run the stage1-r
walk-forward, pool its OOS per-trade R series, and report a moving-block
bootstrap E[R] distribution / CI (one `mc_r_bootstrap` line with an `e_r`
quantile block + P(E[R]<=0)). Completes spec 0075's Monte-Carlo half on top
of iter 1's walk-forward R-reporting.
Engine: `r_bootstrap(rs, n_resamples, block_len, seed) -> RBootstrap` — a
deterministic moving-block bootstrap (non-circular, final block truncated;
block_len=1 = i.i.d. trade-shuffle; clamped to [1,n]; empty -> all-zero),
reusing the existing MetricStats/quantile and SplitMix64.
CLI: parse_mc_args/McArgs split — bare/`--name`/`--trace` route to the
unchanged synthetic seed-resweep `run_mc` (byte-for-byte preserved); the R
path accepts ONLY `--strategy stage1-r`, so a bare `--real` stays a usage
error (mc_rejects_real_flag_with_usage_exit_2 preserved, doc-comment
refreshed). run_mc_r_bootstrap reuses walkforward_family + RMetrics.trade_rs.
Corrective fixes over the plan snippets, both verified: the mc usage string
carries a `usage:` token (the new `["mc", rest @ ..]` arm now handles the
`--real` rejection, and the preserved golden asserts `stderr.contains
("usage")`); `#[allow(clippy::large_enum_variant)]` on McArgs keeps the
unboxed Stage1RGrid field layout.
Scope: single `--real <SYM>` per invocation; cross-symbol pooling is a
deferred follow-on (needs a multi-symbol grammar). Verified: cargo build
clean, full `cargo test --workspace` green (609 passed), clippy -D warnings
clean; bare `aura mc`, SMA walkforward, stage1-r single-run, and C18
round-trip goldens all preserved.
refs #139
|
||
|
|
f286bb85f7 |
feat(0075): walk-forward strategy-selectable + R-reporting (iter 1)
Make `aura walkforward --strategy stage1-r [--real <SYM>]` roll IS->OOS windows, sweep the stage1-r grid in-sample, pick the winner by an R metric (sqn_normalized), run it out-of-sample, and report per-window + pooled OOS R-metrics. The bare SMA walkforward path is byte-identical (its dispatch arm is verbatim today's body; the pooled `oos_r` block is emitted only when a window carries an `r` block). Engine: - RMetrics gains an in-memory `trade_rs: Vec<f64>` (realised R per closed trade), excluded from serde (`#[serde(skip)]`) and from a hand-written PartialEq, so the C18 wire shape and every existing equality assertion / round-trip stay unchanged. summarize_r now retains the per-trade R vector it used to drop. - New `r_metrics_from_rs(&[f64])` reduces a flat (pooled across-window) R series to RMetrics. Its R-distribution arithmetic is copied verbatim from summarize_r (the byte-pinned floats must not be algebraically refactored); the two copies are guarded in lockstep by a cross-reducer equality test. net_expectancy_r = expectancy_r (exact at the Stage-1 cost=0 invariant); conviction_terciles_r = [0,0,0] (per-trade conviction is not pooled). CLI: - walkforward gains --strategy + the four stage1-r grid flags (reusing parse_csv_list / Stage1RGrid); walkforward_family is strategy-dispatched. - Windowed stage1-r helpers: stage1_r_sweep_over (reduce-mode folded IS sweep, O(trades)/member) and run_oos_r (non-reduce OOS run, for the stitched pip-equity curve), plus stage1_r_space. Frictionless Stage-1 R (costs are Stage-2). Verified: cargo build clean, full `cargo test --workspace` green (SMA walkforward, synthetic mc, stage1_r_single_run_output_golden, and the C18 round-trips all preserved), clippy -D warnings clean. Monte-Carlo R-bootstrap is iter 2. refs #139 |
||
|
|
7e4b91ab5f |
feat(0074): source real-path pip from the geometry sidecar; drop the authored floor
The data-server now ships per-symbol geometry sidecars, so aura no longer hard-codes instrument geometry. A consumption grep showed production reads exactly one `InstrumentSpec` field — `pip_size` — at the real-data path; the other eight (incl. `instrument_id`) were constructed but never read. - aura-cli resolves the real-path pip from the recorded sidecar (`instrument_geometry` -> `InstrumentGeometry.pip_size`), refusing (exit 2) on absent geometry. The authored table no longer gates: any symbol with a sidecar + bars now runs (new test: USDJPY, never vetted, resolves its JPY pip 0.01 from the sidecar). - Removed the redundant authored floor: `InstrumentSpec`, `instrument_spec`, `VETTED_SYMBOLS`, and the table-only tests. The `InstrumentGeometry` seam (the surviving pip source) and its nameability guard are kept. - `instrument_id` (the "find out"): no production consumer. The position-event table's `instrument_id` is a decoupled caller parameter; the engine never imports an instrument spec. Dropped with the struct. - The example `pip_size_of` is re-sourced to the sidecar; the pure-structural blueprint tests use a literal pip to stay archive-independent. Speculative deploy-grade fields and the #124 override/floor tiers are deferred to the Stage-2 broker that will consume them, read from the sidecar geometry then. Ledger (C8/C15/#22/#106) updated; #124 collapses to "recorded sidecar geometry -> refuse" in the meantime. Verified: cargo test --workspace (all green), clippy -D warnings clean, cargo doc clean, with the local archive present so the sidecar paths run. refs #124 |
||
|
|
216cc8c417 |
feat(0072): stage1 mean-reversion candidate — EWMA Bollinger fade (#137)
Third price-only Stage-1 R strategy: aura sweep --strategy stage1-meanrev fades deviation from a rolling mean (price above mean+k*sigma -> short, below -> long, latched +-1), feeding the unchanged bias -> RiskExecutor (vol-stop = R) -> flat-1R seam. sigma = Sqrt(Ema((price-Ema(price))^2)) reuses the vol_stop deviation- squared-then-smoothed shape, so the band is built from existing aura-std nodes (Ema/Sub/Mul/Sqrt/LinComb/Add/Gt/Latch) -> ZERO new nodes. No Delay: the current bar legitimately belongs to its own band (causal, C2). Swept knobs: --window (one Ema length ganged across mean+variance) and --band-k; downstream vol-stop R knobs unchanged. An in-file test reads the SHIPPED graph's exposure tap to pin the fade polarity (short above the band, long below) — a latch-set/reset copy-paste from breakout would invert the signal yet leave the fold-vs-raw and window-grid CLI tests green. All 588 workspace tests pass (existing R/sma/momentum/breakout goldens byte-identical); clippy clean. refs #137 |
||
|
|
d845c509d3 |
feat(0071): stage1 breakout candidate — Donchian channel signal (#137)
A second Stage-1 R strategy candidate for the edge-research milestone, a structurally
different trend mechanic than the refuted SMA-momentum: a Donchian channel breakout.
Swaps only the signal leg of the stage1-r graph —
close -> Delay(1) -> {RollingMax,RollingMin}(N) -> {Gt,Gt} -> {Latch,Latch} ->
Sub = bias in {-1,0,+1} — keeping the vol-stop R definition unchanged, so it screens
under the identical R yardstick (clean A/B vs momentum).
- aura-std: new RollingMax / RollingMin nodes (sliding-window extremum via a monotonic
deque, O(1) amortized; mirror the Sma/Delay ring-buffer shape, warm-up skip-emit,
C7/C8). The +/-1 direction latch composes from two existing Latch nodes + Sub (no new
latch type).
- aura-cli: stage1_breakout_graph + a manual-grid stage1_breakout_sweep_family
(fully-bound graph per point, sidestepping parameter-ganging since one channel length
drives both rolling nodes); --strategy stage1-breakout + --channel grid flag; usage
strings updated.
- Causality (C2): one Delay(1) on close feeds both rolling nodes, so each channel covers
close[t-N..t-1] — the current bar is excluded. Pinned by a contrastive e2e test (a
strictly rising series up-breaks every warmed bar; a window including the current bar
never could) plus a +/-1-latch-hold test.
All existing goldens byte-identical (stage1-r/sma/momentum untouched); folded-no-trace
== raw-trace metrics; workspace tests + clippy green.
refs #137
|
||
|
|
eb8653488f |
feat(0070): wire folding sink reductions into the stage1-r sweep (closes #138)
Completes the M3 fix for BLOCKER #138. The no-trace stage1-r sweep now folds its reductions online: `stage1_r_graph(reduce=true)` wires `SeriesReducer` (equity, exposure -> one summary row each) and `GatedRecorder` (gated on `closed_this_cycle` -> only the trade rows + the final row), and the folded consumer rebuilds `RunMetrics` from those compact outputs. Per-member memory drops from O(cycles) (~2 GiB over ~5.5M one-minute bars) to O(trades), so full-history multi-member sweeps no longer exhaust RAM+swap. The `r_equity` tap is omitted when reducing (it is consumed only by the `--trace` persistence path). `reduce = trace.is_none()`: the `--trace` path and the single `run_stage1_r` keep the raw recorders + `summarize`/`summarize_r` over full rows — the byte-for-byte reference. A new CLI-seam test (`sweep_strategy_stage1_r_folded_no_trace_metrics_equal_raw_trace_metrics`) proves the folded no-trace sweep reports byte-identical per-member metrics to the raw `--trace` path. Verified: cargo test --workspace green (42 suites); cargo clippy --workspace --all-targets -- -D warnings clean; stage1-r goldens byte-identical. closes #138 |
||
|
|
54a5d68f51 |
feat(stage1-r-sweep): grid signal+stop timescales via CSV flags
Enabling change for the strategy-research run: `aura sweep --strategy
stage1-r` gains four optional comma-separated grid flags `--fast`,
`--slow`, `--stop-length`, `--stop-k` that become the sweep's grid axes
over the four stage1_r_graph knobs (sma fast/slow length, vol-stop
length, vol-stop k). The family is their cartesian product. This lets a
slow time-series-momentum edge be screened across timeframes — Phase 0
showed fast (2/4-bar) signals are chop-dominated noise on M1, so any
momentum edge lives at slower timescales, and the stop timescale must be
tunable alongside the signal.
Mechanism:
- aura-composites: factor vol_stop and risk_executor into a single
shared topology body each (vol_stop_inner / risk_executor_inner), with
the vol-stop knobs either BOUND (the original constant-bind form,
byte-identical) or left OPEN and named `stop_length` / `stop_k` so
their slots surface in param_space as sweepable axes. New
risk_executor_vol_open(risk_budget) is the gridding sibling of
risk_executor(StopRule::Vol{..}, ..). One body, two arms → topology
cannot drift; a member at any (length, k) matches the bound form
byte-for-byte.
- aura-cli: parse_csv_list (generic FromStr, rejects non-numeric/empty),
pure unit-testable parse_sweep_args, all four knobs gridded via
.axis(); absent flags fall back to today's exact defaults (fast {2,3},
slow {6,12}, stop_length {3}, stop_k {2.0}).
Default behaviour byte-identical: all existing goldens/sweep tests stay
green (541 passed, 0 failed; clippy -D warnings clean). Frictionless
Stage-1 R unchanged.
refs #137
|
||
|
|
e94b79eb98 |
test(stage1-r-sweep): RED — grid all four knobs from CSV flags
Executable-spec for the strategy-research enabling change: expose the
stage1-r sweep's hardcoded timescales as CLI grids so a slow
time-series-momentum edge can be screened across timeframes.
- sweep_strategy_stage1_r_grids_all_four_knobs_from_csv_flags (RED):
`aura sweep --strategy stage1-r --fast 240 --slow 960
--stop-length 240 --stop-k 2.0` must yield a 1-member family whose
member params are fast=240, slow=960, stop_length=240, stop_k=2.0.
Fails today: the four flags are rejected (exit 2), gridding absent.
- sweep_strategy_stage1_r_no_flags_keeps_the_default_grid (guard,
green today): a no-flags sweep still yields the {2,3}×{6,12} 4-member
family with the stop pinned at 3 / 2.0, so existing goldens stay
byte-green once the feature lands.
GREEN side follows via implement mini-mode.
refs #137
|
||
|
|
89d967a940 |
feat(0067): SQN100 metric + stage1-r sweep --trace
Two additive follow-ups to the cycle-0065/0066 R-sweep surface. Part A (#130) — n-normalized SQN (SQN100): RMetrics gains sqn_normalized = (mean_R/stdev_R)·√(min(n,100)), a turnover-robust ranking objective comparable across sweep members with different trade counts. The raw sqn is kept byte-identical (computed sqrt(n)*mean/sd, NOT via a factored shared ratio, so the existing metric does not drift even by 1 ULP). The field carries #[serde(default)] for C18 back-compat (a pre-0067 r: block deserialises with sqn_normalized = 0.0). The registry learns Metric::SqnNormalized (opt-in; the default ranker and raw sqn are unchanged), so `runs family rank sqn_normalized` works. The 0066 single-run golden is re-baselined (the r: block gains the field; for n=3 ≤ 100 the cap is inactive so sqn_normalized == sqn). Part B (#135) — wire --trace for the stage1-r sweep: stage1_r_sweep_family now honours --trace, persisting each member's equity/exposure/r_equity under runs/traces/<n>/<member_key>/ via the existing persist_traces_r (mirroring momentum_sweep_family); the interim run_sweep refusal guard is dropped. --trace is now symmetric across all three sweep strategies and a swept member is chartable (chart <n>/<member> --tap r_equity). Tests: SQN100 cap / below-cap / empty + serde back-compat (engine); rank-by sqn_normalized + unknown-metric message (registry); an E2E guard that both SQN values fold from recorded records and agree below the cap (the cross-crate column seam); inverted stage1-r --trace persistence + a CLI rank-by- sqn_normalized integration. Full workspace suite green (527 passed, 0 failed); clippy --all-targets -D warnings clean. Derived fork decisions logged on #130 and #135 (cap = fixed const 100; opt-in metric, default ranker unchanged; serde(default); Part B mirrors momentum_sweep_family + reuses persist_traces_r). closes #130 closes #135 |
||
|
|
499121e5c8 |
fix(stage1-r): record fixed R-defining params in swept member manifests
Cycle-0066 close audit found a C18/C10 gap: a stage1-r sweep member's manifest recorded only the floated knobs (fast.length, slow.length) via zip_params, omitting the fixed R-defining params (stop_length, stop_k, bias_scale) that the single run records. A family member must be reproducible from its own manifest (C18), and the constant stop -- which defines 1R -- must be visible so cross-member SQN comparability (C10) is auditable from the family record, not just the swept fast/slow knobs. The sweep run_one now appends the three fixed params, matching the single run; the integration test pins their presence per member. Verified: full workspace suite 521 passed / 0 failed; clippy -D warnings clean. refs #133 |
||
|
|
b8ed6f4858 |
feat(stage1-r): R sweep families + rank-by-SQN
Realizes #133 (cycle 0066): SQN becomes the single-number objective for ranking a Stage-1 sweep family by R signal quality. aura-registry — metric_cmp learns three higher-is-better R metrics (sqn, expectancy_r, net_expectancy_r) that reach into RunReport.metrics.r; a member with r:None sorts to the worst position (treated as -inf via total_cmp), so a pip-only family ranked by an R metric degrades to ordinal order without error. The UnknownMetric "known:" list is extended. No persistence change — the family store already carries the r block. aura-cli — `aura sweep --strategy stage1-r` sweeps the stage1-r harness over a fast×slow SIGNAL grid (4 members), each folding the dense R-record via summarize_r so its RunReport carries r:Some(..). The grid varies only the signal: the stop and sizing are held fixed because risk_budget is R-invariant (#128) and bias.scale is sign-only under flat-1R (both degenerate axes for the ranked metric), and because the stop defines the R unit — varying it would break cross-member SQN comparability (the motivation for the deferred SQN100, #130). The stage1-r topology is extracted into a shared stage1_r_graph(.., fast_len, slow_len) helper: the single run binds Some(2)/Some(4) (output byte-unchanged), only the sweep floats the knobs. Per-member --trace tracing is deferred (a follow-up): rather than accept the flag and silently write nothing (a quality-review catch), `aura sweep --strategy stage1-r --trace` is refused explicitly (exit 2); --name records the rankable family. Verification — a golden characterization test (stage1_r_single_run_output_golden, added against HEAD before the refactor) verifies the helper extraction is byte-preserving; full workspace suite 521 passed / 0 failed; clippy --all-targets -D warnings clean. The grounding-check that signed the spec first BLOCKed a false "already byte-pinned" claim, which is why the golden is added rather than assumed. All load-bearing forks (signal-only grid, None-sorts-worst, --trace refusal, helper extraction, golden shape) are recorded on the #133 decision log. closes #133 refs #117 #130 |
||
|
|
3d3baaeb24 |
refactor(stage1-r): dedicated broker label + intern col-port names (no per-build leak)
Two cosmetic smells the cycle-0065 close audit flagged (#132), neither a correctness issue: 1. Broker label. run_stage1_r reused sim_optimal_manifest, recording broker: "sim-optimal(pip_size=...)" for a harness that also runs a RiskExecutor branch. It now overrides the label to "sim-optimal+risk-executor(pip_size=...)" so the dual-tap harness reads honestly. A new assertion in run_stage1_r_synthetic_folds_an_r_block pins it. 2. col[i] leak. stage1_r_blueprint did format!("col[{i}]").leak() per PositionManagement field (14 strings) to satisfy the &'static str port-name API -- harmless for a CLI one-shot, but a leak-per-build smell once the harness is reused in a sweep / Monte-Carlo loop (the #133 direction). Replaced with a process-wide LazyLock<Vec<String>> (COL_PORTS), interned once: any number of builds now reuse the same 14 strings. (#132 deferred this to #133, but the intern is robust to any reuse pattern, so it lands here cleanly.) Behaviour-preserving (the port names and the recorded R-record are unchanged); the broker label is the only observable change, and it is pinned. Verified: clippy --all-targets -D warnings clean; full suite 514 passed / 0 failed; live `aura run --harness stage1-r` reports "broker":"sim-optimal+risk-executor(pip_size=0.0001)". closes #132 |
||
|
|
68605b7d88 |
refactor(stage1-r): rename the strategy-output param namespace exposure -> bias
Complete the deferred rename tail from cycle 0065's #126 (which renamed only the typed metric, keeping the param namespace uniformly "exposure" so the single-run rename would not smuggle in a ~30-site sweep-pinned change). Now renamed together as one consistent unit, all driven by the Bias node's instance name: - Bias::builder().named("exposure") -> .named("bias") (the harness builders + the engine fixtures in test_fixtures.rs / blueprint.rs); - the derived knob path exposure.scale -> bias.scale (Composite::param_space derives the knob from the node name) -- the sweep .axis/.range bindings and .with() points (blueprint.rs, main.rs), the walkforward ParamSpecs (walkforward.rs), the member_key dir names, and the JSON byte-pins in cli_run.rs / report.rs / main.rs tests; - the exposure_scale manifest param label -> bias_scale (the single-run manifest builders + the streaming_seam / real_bars fixtures). The rename is surgical: the three targets (named("exposure"), exposure.scale, exposure_scale) are syntactically distinct from the deliberately-kept "exposure" forms, which are untouched -- SimBroker's pre-reframe exposure input slot + prev_exposure + the exposure-integral; the LongOnly / gate / latch ports named "exposure"; and the on-disk "exposure" tap label (the recorded bias series that feeds `chart --tap exposure`, decoupled from the node name via ColumnarTrace::from_rows("exposure", ...)). No on-disk back-compat break: the knob path is a runtime param address, and recorded params in old runs.jsonl / families.jsonl are inert data strings (a pre-existing recorded "exposure.scale" still loads, it is just data). INDEX.md cycle-0065 realization note amended: the param namespace is recorded as the now-completed tail; the SimBroker slot + the on-disk tap label remain the deliberate permanent keeps (#117), and exposure_sign_flips stays a serde alias. Verified: cargo build --workspace --all-targets clean; clippy --all-targets -D warnings clean; full suite 514 passed / 0 failed (source + byte-pins renamed together); live `aura run` emits "bias_scale", `aura sweep` emits "bias.scale". closes #134 refs #126 #117 |
||
|
|
5c6de45b6e |
fix(cli): list valid taps on bad --tap, make --help uniform across subcommands
Two CLI-UX defects the Stage-1 R fieldtest surfaced (#131): 1. Tap discovery. `aura chart <name> --tap <bad>` rejected an unknown tap but the error omitted the valid names. filter_to_tap now enumerates the run's available taps in the message ("... (available: equity, exposure)"), so the user can correct a typo instead of guessing -- refuse, but help. 2. Uniform --help. `aura chart --help` errored with "unexpected chart argument '--help'" (exit 2) and `aura run --help` leaked usage to stderr via the error path, while bare `aura --help` printed to stdout (exit 0). main() now intercepts `--help`/`-h` anywhere in the invocation, prints usage to stdout, and exits 0 -- uniform for every subcommand. The now-dead bare-flag match arm is removed (the early intercept covers it). RED-first: the existing chart_tap_nonexistent test gains an available-taps assertion (was: message had no names), and a new per_subcommand_help test pins `aura <sub> --help` -> stdout usage + exit 0 for run/chart/sweep/walkforward/ mc/graph/runs (was: chart --help exited 2). Unknown-subcommand stays exit 2. Verified: clippy --all-targets -D warnings clean; full suite 514 passed / 0 failed (one new test); live `aura chart --help` prints usage to stdout, exit 0. closes #131 |
||
|
|
0288878ad2 |
refactor(cli): remove the redundant --macd flag, superseded by --harness macd
The --macd flag was a back-compat alias for --harness macd, added in cycle 0065 iter-3 while the uniform `--harness <name>` selector was introduced. With that selector in place the alias is pure redundancy, so drop it: --harness <name> is now the sole way to pick a built-in harness. Removed from parse_run_args: the macd_flag accumulator, its match arm, and the (harness, macd_flag) conflict resolution -- harness selection collapses to `harness.unwrap_or(HarnessKind::Sma)`. Both usage strings drop `[--macd]`; the parse_run_args doc and the parse_real_args note no longer cite the flag. The MACD harness itself is untouched: run_macd, the macd() composite, and `--harness macd` all stay. Tests updated -- the alias test becomes a positive `--harness macd -> Macd` test plus an assertion that `--macd` is now rejected as an unknown token; the "repeated --macd" edge is dropped (the flag is gone, and repeated-flag strictness is still covered by --harness/--trace). INDEX.md: the two realization notes that documented `--macd` as a live run form updated (`aura run [--real ...]`; `run --harness macd`). The historical graph-viewer retirement note and the frozen fieldtest capture are left as the record of their time. Verified: clippy --all-targets -D warnings clean; full suite 513 passed / 0 failed (assertions rewritten in place, no test count change); live smoke -- `aura run --harness macd` emits a RunReport, `aura run --macd` exits with the updated usage. |
||
|
|
1a6eafa056 |
refactor(composites): extract Stage-1 composite-builders into their own crate
Dissolve the aura-engine -> aura-std dependency by moving the vol_stop /
risk_executor composite-builders out of aura-engine into a new aura-composites
crate. This restores the engine's domain-free layering, which the
composites-in-engine placement (cycle 0065) had inverted.
Why: the engine routes type-erased Scalar records and never names a concrete
node -- summarize_r reads the PositionManagement record positionally, by column
index, without linking aura-std (which is exactly why aura-std was a dev-only
dependency before 0065). Parking the node-naming composite-builders inside
aura-engine forced the engine's production code to reference the node library
for the first time (aura-engine -> aura-std), an upside-down layering: the
engine is the foundation the node library builds upon, not the reverse.
The clean home is a dedicated crate that depends on BOTH the engine's
GraphBuilder and the std nodes, leaving each lower crate untouched:
aura-composites -> { aura-engine, aura-std } -> aura-core (acyclic)
aura-engine -> aura-core only (domain-free again)
aura-std -> aura-core only (lean node library)
aura-std reverts to a [dev-dependency] of aura-engine (the engine's own E2E
tests -- stage1_r_e2e, random_sweep_e2e, ger40_breakout -- still drive real
std nodes through a bootstrapped Harness; that test-only edge creates no cycle).
Moved with history (git mv):
- crates/aura-engine/src/composites.rs -> crates/aura-composites/src/lib.rs
- crates/aura-engine/tests/risk_executor.rs -> crates/aura-composites/tests/
- crates/aura-engine/tests/vol_stop_composite.rs -> crates/aura-composites/tests/
Consumers rewired: aura-cli imports risk_executor / StopRule from aura_composites;
the moved tests import the builders from the crate under test. The intra-doc link
to RollMode and the module rationale doc are updated for the new home.
Behaviour-preserving: workspace build clean; clippy --all-targets -D warnings
clean; full suite 513 passed / 0 failed, identical to baseline (the two moved
test files run unchanged under aura-composites); cargo doc --no-deps clean (no
broken intra-doc links).
|
||
|
|
c6c1d3bb10 |
feat(stage1-r): rename the exposure_sign_flips metric to bias_sign_flips (#126)
Complete the typed-field half of the exposure->bias rename - the one the cycle-0065
close audit flagged as aging worst (a serde-stable on-disk key).
- RunMetrics.exposure_sign_flips -> bias_sign_flips with
#[serde(alias = "exposure_sign_flips")], so legacy runs.jsonl still deserialises
(pinned by the legacy-read tests, which keep the old key); new output serialises
bias_sign_flips (the byte-pin tests updated to the new key).
- The registry rank metric: Metric::ExposureSignFlips -> BiasSignFlips, the rank
string accepts BOTH "bias_sign_flips" and "exposure_sign_flips" (CLI back-compat),
the error/listing updated; the rank tests exercise both names.
- The MC aggregate field renamed too (McAggregate is hand-rendered to JSON, not
serde-derived - no alias needed).
Scope held to the typed metric. The strategy-output PARAM NAMESPACE - the
.named("exposure") Bias instance, its exposure.scale knob path (Composite::param_space
derives the knob from the node name), and the exposure_scale manifest label - is a
coherent ~30-site, sweep-pinned surface (the implement loop correctly flagged renaming
the knob as an unscoped expansion touching walkforward + the cli_run byte-pins). Kept
uniformly as "exposure" and deferred to a follow-on so the cluster renames as one
consistent unit. SimBroker's pre-reframe exposure concept and the on-disk "exposure"
tap label stay by the #117 fork decision.
cargo test --workspace green; clippy --workspace --all-targets -D warnings clean; a
live `aura run --harness stage1-r` emits "bias_sign_flips".
closes #126
refs #117
|
||
|
|
a6fc48daa7 |
feat(stage1-r): operate the R layer from the CLI (iter 3)
`aura run --harness stage1-r [--real <SYM>] [--trace <n>]` now runs a Stage-1 R
backtest and prints its signal-quality metrics (the R block) in the RunReport
JSON - one shell call, the research loop's primary verb, ergonomic for Claude
to drive.
What shipped:
- vol_stop + risk_executor promoted from test fixtures to public aura-engine
composite-builders, with a StopRule{Fixed,Vol} axis (C11). aura-std becomes a
normal dependency of aura-engine (the composites are the engine's convenience
layer over the standard nodes - the sibling tier of summarize_r; the graph
stays acyclic: aura-engine -> aura-std -> aura-core). Both fixtures call the
public fns; a Vol-backed RED test pins the new match arm.
- stage1_r_blueprint fans one bias into BOTH a SimBroker (pip) and the
RiskExecutor (R), so one report carries both yardsticks honestly. run_stage1_r
folds summarize (pip) + summarize_r (R, round_trip_cost 0.0 - Stage-1 is
frictionless signal quality) and sets RunMetrics.r = Some(..). An r_equity tap
(cum_realized_r + unrealized_r via LinComb) persists through a separate 3-tap
persist_traces_r and renders via the existing `aura chart --tap r_equity`.
- `aura run --harness <sma|macd|stage1-r>`: a parse_run_args tokenizer replaces
the five `run` literal-slice arms so --harness/--real/--from/--to/--trace
compose in any order; --macd kept as a back-compat alias; --harness sma the
default. A compile-time match over Rust-authored built-ins - not a runtime node
registry, not a DSL (C9/C17): the CLI runs an authored harness, it does not
wire one.
Back-compat: --harness sma/macd leave RunMetrics.r = None, so skip_serializing_if
keeps pip-only and legacy runs.jsonl JSON byte-unchanged; the plain-run tap list
stays ["equity","exposure"].
Refactor beyond the plan (verified behavior-equivalent): the old standalone
parse_real_args was orphaned by the new tokenizer, so it and its now-redundant
unit test were removed and run's --real parsing inlined into parse_run_args. The
--real strictness (empty-symbol, non-i64 ms, repeated-flag, window-requires-real
-> exit 2) is preserved and now pinned by
parse_run_args_rejects_malformed_real_and_repeated_flags (added to restore the
deleted coverage). All five run_real integration tests stay green.
Verification: cargo build/test --workspace green (every crate, 0 failed); clippy
--workspace --all-targets -D warnings clean. The full diff was adversarially
reviewed (run-arg refactor equivalence against the removed function, harness
wiring, C1/C2/C9/C17, the serde back-compat pin) - verdict SOUND; the one flagged
coverage gap is closed here.
Follow-on (noted, not done): the stage1-r manifest reuses the
"sim-optimal(pip_size=...)" broker label; a dedicated "risk-executor" label would
read more honestly for a dual-tap harness that runs a RiskExecutor branch.
closes #129
refs #117 #128
|
||
|
|
2c43296c2c |
feat(stage1-r): bias rename + stop-rule + position-management + summarize_r (iter 1)
Iteration 1 of the Stage-1 "R-based signal quality" cycle (spec 0065): a strategy's signal quality, measured in R on its unsized bias stream, feed-forward. New discrete-trade machinery, NOT a SimBroker extension. What lands: - exposure -> bias (refs #126): the Exposure node/type/file/output-field renamed to Bias (computation unchanged: clamp(signal/scale,-1,+1); the SEMANTICS change — the output is the unsized strategy bias, sizing leaves the strategy). Scoped to the semantic core; behaviour-preserving cosmetics are deferred (see below). - stop-rule nodes (refs #119): VolStop = k*EMA(|price - prev_price|) (one fused node; the volatility that defines 1R, close-to-close — true-range ATR deferred, needs OHLC) + FixedStop (constant, the test fixture / fixed-vs-vol structural-axis sibling). - PositionManagement (refs #127): the stateful heart. Latches the entry-cycle stop distance as the FROZEN R-denominator (never re-read), marks/exits no-look-ahead like SimBroker (a position earns from the next cycle; an exit realises against the cycle's close), and emits ONE dense per-cycle R-record (C8). Stop-outs are NOT capped at -1R (a gap through the stop realises R < -1 — the honest loss tail); R is computed size-invariantly (size = (exit-entry)*dir / latched_dist cancels). The trade ledger is the rows where closed_this_cycle; the R-equity is cum_realized + unrealized; a position open at window end is the last row (open=true) — explicit, never silent MtM. - summarize_r + RMetrics (refs #129): the post-run fold (NOT an in-graph node — recorders drain post-run). E[R], win-rate, profit-factor, avg win/loss R, by-trade max R-drawdown, n_open_at_end (window-end force-close). SQN / conviction terciles / net-of-cost / RunMetrics.r are iteration 2. Design adversarially hardened before implementation (12-juror refute panel; decisions on #117). Ratified implementer deviations from the plan snippet, verified by hand: - col-4 `direction` tracks the OPEN position at cycle end (window-end synthesis; names the reopened leg on a reversal), falling back to the closed trade's dir only when flat. summarize_r does not read col 4; the R-metrics are unaffected. - .named("exposure") kept on the 4 previously-unnamed Bias nodes so the auto-derived param-path stays exposure.scale (no manifest/dir-name drift) — the unnamed nodes would otherwise flip to bias.scale. - intra-doc links [`Exposure`] -> [`Bias`] in sim_broker/latch + the sample-model test pin (cosmetic, behaviour-neutral; broken intra-doc links fail cargo doc). - the E2E drives a full bootstrapped Harness (stronger than the plan's direct-node drive — exercises the real cross-crate producer->consumer seam). Deferred (behaviour-preserving, separate cosmetic pass — NOT this iteration): RunMetrics.exposure_sign_flips / Metric::ExposureSignFlips, SimBroker/LongOnly "exposure" input ports, the "exposure" tap/trace names, and the .named("exposure") instance labels. Iteration 2: the Sizer seam, the RiskExecutor composite (+ the Veto documented-seam), summarize_r enrichment, RunMetrics.r, and the CLI/recording surface. Verification (orchestrator-run, not agent-claimed): - cargo build --workspace: clean. - cargo test --workspace: all green, 0 failed (incl. the new bias/stop_rule/ position_management unit tests, the summarize_r arithmetic tests, and the stage1_r_e2e capstone + layout guard). - cargo clippy --workspace --all-targets -- -D warnings: clean (exit 0). - Keystone RED tests pass: no_lookahead_bias_exit_realises_the_held_move, stop_out_is_not_capped_at_minus_one_r, no_gap_stop_is_exactly_minus_one_r, reversal_closes_one_leg_and_reopens, open_at_window_end_is_carried_on_the_last_row. refs #117 #119 #126 #127 #129 |
||
|
|
2957561c30 |
fix(chart): mean-decimate the bounded exposure series instead of a min/max band
The serve-time decimation (#108) reduced every series by per-bucket min/max. That is the right envelope for a cumulative equity curve but wrong for the bounded exposure stream (C10, f64 ∈ [-1,+1]): over a multi-year window each ~hundreds-of-bars bucket straddles sign flips, so min/max is ±1 in nearly every bucket and the exposure collapses to a solid -1..+1 band that reads as per-point oscillation — even for a calm strategy (a 50/200 member flips only 0.81% of bars yet still bands out). Make decimation tap-aware (RED-first, #111): a new `ReduceKind {MinMax, Mean}` on each Series, set by the chart builders via `reduce_for_tap` (exposure -> Mean, else MinMax). `decimate` honours it — a Mean series emits the per-bucket mean (its net/duty-cycle level) in both spine slots, a MinMax series keeps min/max. The shared spine, the equity rendering, and the page payload are unchanged (reduce is server-side only, `#[serde(skip)]`). Verified on real GER40 1y M1: the served exposure series goes from a ±1 band to a smooth net-level line in [-0.38, +0.34]. Rendering the min/max envelope honestly (range bars / OHLC, vs today's polyline) is the deferred other half — filed as #112. Verified: cargo test --workspace = 447 passed / 0 failed (incl. the RED-then-GREEN decimate_mean_reduces_a_bipolar_series_to_its_bucket_level); clippy -D warnings clean. closes #111 refs #112 |
||
|
|
476342d7b1 |
feat(chart): decimate served pages + run-context header (visual World cut 2)
Hardens the families-comparison chart page (#107) so it is openable and legible on real multi-year data. Two halves, both confined to the CLI render path (engine + registry untouched, C14): #108 — serve-time min-max decimation. A pure `decimate(ChartData, buckets)` transform partitions the shared union-ts spine into <=~2000 buckets and emits each series' per-bucket min+max (nulls preserved), bounding the served page to a few thousand points regardless of the underlying M1 point count. Applied in emit_chart on both the single-run and family paths; a no-op under budget (every existing fixture). Full recorded data stays on disk; only the page is thinned. For a walk-forward family the null-fill blow-up collapses for free (an all-null bucket stays null). Deterministic (C1). #102 — a run-context header. A `ChartMeta` (name/commit/window/broker/seed/taps, + member count for a family, + bound params for a single run) is built from the RunManifest in build_chart_data / build_comparison_chart_data (which gain a `name` arg), injected into window.AURA_TRACES.meta, and rendered by a new pure buildHeader in chart-viewer.js (headless .mjs-guarded). The family window is the SPAN across members (min from, max to), not the first member's window — the only reading that labels a disjoint walk-forward family's true OOS coverage (it collapses to the shared window for sweep/MC). Reuses the existing render_value for param display (no new Scalar Display, keeping C7's tag-only param plane). The earlier "manifest is NOT carried into the page" render pin is flipped to a positive one. Verified: cargo test --workspace = 446 passed / 0 failed; cargo clippy --workspace --all-targets -D warnings clean. New coverage: 5 decimate unit tests, single-run + family meta wiring (incl. the span-window invariant), a buildHeader headless guard, and two end-to-end cli_run tests pinning the served-page meta at the binary boundary. closes #108 closes #102 |
||
|
|
4c64feb9ed |
feat(chart): aura chart <family> overlays a family's members; --tap + write-guard
`aura chart <name>` now branches on the name's on-disk kind: a single run charts
its taps as before; a FAMILY overlays one tap (default equity) across all its
members in one self-contained uPlot page, consistent across sweep / MC /
walk-forward. Members share ONE y-scale (they measure one quantity — that is what
makes the overlay comparable, unlike the single-run overlay of different taps),
and align on the union-ts spine via the existing join_on_ts: sweep/MC members
share the window (true overlay), walk-forward members are disjoint OOS windows
(null-complementary -> the stitched curve). One mechanism, three correct readings.
- build_comparison_chart_data (one labelled Series per member, shared y_scale_id);
filter_to_tap (single-run --tap restricts to one tap; no --tap = all taps,
unchanged); parse_chart_args (`<name> [--tap <t>] [--panels]`, any order);
emit_chart name-kind branch; USAGE updated.
- Write-guard: ensure_name_free is now called once per tracing command (run /
run --macd / run --real -> Run; sweep / mc / walkforward -> Family) so a name
cannot be reused across kinds — name resolution stays a total function.
Refuse-don't-guess throughout (exit 2, never panic). Engine untouched (C9/C14).
Tasks 2-3 of plan 0061; Task 1 (registry) landed in
|
||
|
|
094d63bc47 |
fix(family-stdout): unify family-wrapper key order with the store
The sweep / walk-forward / mc family-member stdout lines routed the embedded RunReport through serde_json::json! -> a serde_json::Value, which re-alphabetizes the manifest keys (broker-first) and, for mc, even reordered the top-level keys (seed after report). That diverged from the commit-first declaration order that RunReport::to_json and the stored families.jsonl already use. Splice the pre-serialized report into a hand-built line via family_member_line / mc_member_line so each family-member stdout line is byte-identical to its families.jsonl record (commit-first manifest, seed between family_id and report). RED-first: the two new key-order tests fail against the old json! helpers and pass after the format! rewrite. closes #99 |
||
|
|
99fd32b1f9 |
feat(real-family): realistic strategy lengths over real data
The built-in family grid was calibrated for the synthetic demo streams (18/60
bars), where trend SMA 2-5 / MACD 2-4-3 are the only lengths that warm. Over real
M1 data those are noise — ~9000 exposure sign-flips per month, a strategy trading
the bid-ask wiggle, not a trend.
DataSource::strategy_lengths() makes the grid data-kind-dependent (like
wf_window_sizes): synthetic keeps {2,3}x{4,5} + MACD 2/4/3 (byte-unchanged); real
uses {50,100}x{200,400} trend SMAs + standard 12/26/9 MACD. Threaded through
sweep_family + sweep_over (the walk-forward in-sample sweep). Over real EURUSD the
monthly flip count drops from ~9000 to ~130-310 — a genuine trend cross.
This is a demo-strategy calibration patch (ledger C22 amendment notes it as such);
the real answer is project-authored strategies owning their own grid (C9), deferred
to the project-env work.
Verified: cargo test -p aura-cli (all green, synthetic byte-unchanged),
clippy -D warnings clean; real sweep probe shows ~130-310 flips/month.
refs #106
|
||
|
|
8e5d14b2bb |
feat(real-family): --real parser + dispatch wiring; complete the feature
Plan 0060 Tasks 4-5 (#106): wire `aura sweep|walkforward --real` end to end. - A shared RealWindowGrammar accumulator holds the `--real <SYMBOL> [--from <ms>] [--to <ms>]` grammar (flag-repeat strictness, empty-symbol + window-without-real rejection) for both parsers, mirroring parse_real_args. - parse_sweep_args grows the DataChoice 4th element; parse_walkforward_args is new (no --strategy axis). The sweep + walkforward dispatch arms become rest-matched, build the DataSource via from_choice (build-or-refuse on un-vetted symbol / absent data), and run. USAGE documents the --real tails. mc stays exact-matched, so `aura mc --real` falls through to usage + exit(2) — Fork A: MC excluded (its seed varies a synthetic price-walk, undefined over real data). - Gated integration tests (skip on no local data): sweep --real EURUSD persists 4 portable member dirs over real bars and charts; walkforward --real EURUSD persists one oos<ns> dir per rolling window; same real sweep twice is byte-identical (C1). Un-gated: un-vetted symbol, window-without-real, and mc --real each refuse with exit 2. - Remove the now-redundant `#[allow(dead_code)]` from the Task-1 provider: every const, both enums, and every method are live once the wiring lands. Verified end-to-end over real EURUSD data: sweep --real -> 4 members; walkforward --real over 2024 -> 9 rolling OOS windows; all refusals exit 2. Full workspace test + clippy -D warnings green. closes #106 |
||
|
|
2a6ca47acd |
feat(real-family): thread DataSource + pip through the family builders
Plan 0060 Tasks 2-3 (#106): the family builders now take a &DataSource and draw their source, pip, window, and walk-forward roller sizes from it instead of the hardcoded synthetic VecSource / SYNTHETIC_PIP_SIZE. - sample_blueprint_with_sinks / momentum_blueprint_with_sinks take a pip_size (un-hardcode the SimBroker); every caller threads it. - sweep_family / momentum_sweep_family / walkforward_family / sweep_over / run_oos take &DataSource; run_sweep / run_walkforward take a DataSource and the dispatch passes DataSource::Synthetic (the --real parser is the next step). Synthetic behaviour is byte-unchanged. - Fix a design gap the implementer correctly bounced on: walk-forward is a windowed consumer whose span comes from the 60-bar walkforward_prices, NOT the 18-bar showcase that full_window() returns. Added DataSource::wf_full_span() (synthetic walk-forward span; real = the probed --from..--to) and pointed walkforward_family at it. Without it the synthetic roller (24,12,12) over span (1,18) exits SpanTooShort. Verified: cargo test -p aura-cli (all green, synthetic byte-unchanged), cargo clippy -p aura-cli --all-targets -D warnings (clean). refs #106 |
||
|
|
cf102172ec |
feat(real-family): DataSource provider + shared real-data helpers
Foundation for `aura sweep|walkforward --real` (#106, plan 0060 Task 1). - New CLI-side `DataChoice` (parsed) / `DataSource` (opened) provider: the synthetic-vs-real source choice as one type, supplying a member's source, pip, full/windowed sources, and walk-forward roller sizes. Not yet wired into the family builders (plan Tasks 2-5); `#[allow(dead_code)]` marks the wired-later items, to be removed once the builders thread `&DataSource`. - DRY: `run_sample_real`'s inline pip-refusal / no-data refusal / probe-window logic is extracted to shared `instrument_spec_or_refuse` / `no_real_data` / `probe_window` helpers, now reused by `DataSource::from_choice` / `full_window` instead of duplicated. - Real walk-forward roller-size constants (90d / 30d / 30d in ns, Fork D/F). The synthetic provider is consumer-dependent by design (full-window consumers draw showcase_prices; windowed consumers draw walkforward_prices) so the byte-unchanged guarantee holds across both family paths. Verified: cargo test -p aura-cli (all green incl. byte-unchanged synthetic path), cargo clippy -p aura-cli --all-targets -D warnings (clean). Full-workspace gate at iter close after Tasks 2-5. refs #106 |
||
|
|
0b73a75d4b |
feat(momentum): bool-param demo strategy + aura sweep --strategy selector
Proves the #105 generic portable member key end-to-end: a demo strategy whose params have nothing in common with the SMA-cross demo, swept by the same machinery, with a bool axis surfacing — conformant — in the member dir names. - momentum strategy: price -> Ema(length:i64) -> Sub(price-ema) -> Exposure(scale:f64) -> LongOnly(enabled:bool) -> SimBroker. Three swept params of three kinds incl. a bool; the exposure sink taps the post-gate exposure so the bool's effect is in the trace. - aura sweep gains --strategy <sma|momentum> via a pure, unit-tested parse_sweep_args (mirroring parse_real_args); default = SMA-cross, so the existing bare / --name / --trace forms are unchanged. - `aura sweep --strategy momentum --trace mom` persists 8 member dirs named e.g. ema.length-5_exposure.scale-0.5_longonly.enabled-true — the bool in the dir name, every name matching [A-Za-z0-9._-], each chartable. - ledger (docs/design/INDEX.md): the C22/#101 member-key note's sweep clause amended to the generalised portable form (MC seed{N} / walk-forward oos{ns} unchanged). Self-verified: cargo build --workspace, cargo test --workspace (all green incl. momentum_param_space_is_ema_exposure_longonly, the 8-point determinism test, the parse_sweep_args grammar test, and the momentum_sweep_trace integration test asserting 8 portable bool-bearing dirs + a chartable member), cargo clippy --workspace --all-targets -D warnings. refs #105 |
||
|
|
27f850dc52 |
feat(sweep-key): generic filesystem-portable member key + LongOnly bool node
Generalises the #105 foot-gun: the family-member trace key was derived from two hardcoded axis names (f{fast}s{slow}), collision-free only over the built-in SMA grid. It is now derived from the axes that actually VARY (SweepBinder::varying_axes) and rendered as a filesystem-conformant directory component: - charset restricted to [A-Za-z0-9._-] (valid on Linux/Windows/macOS, also URL-path- and cloud-sync-safe); any other byte sanitised; - case-less values (i64/timestamp decimal, bool true/false, f64 via Display, no scientific notation) so two members of one family never differ only by letter case -> no silent overwrite on case-insensitive filesystems (NTFS/APFS); - length-capped (MAX_KEY=200) with a version-stable FNV-1a fallback for the unbounded-grid edge. Pinned singleton axes are omitted; the SMA sweep's member dirs change from f2s4 to signals.trend.fast.length-2_signals.trend.slow.length-4 (its --trace integration test + the stale prose updated accordingly). Adds the first aura-std node with a bool *param*, LongOnly (a long-only exposure gate: enabled=true clamps short exposure to >=0, false passes through) — the block the momentum demo strategy (next commit) sweeps to prove the key generic over a bool axis. The engine change is one additive pure read accessor; no existing signature changes, trace state stays out of the engine. Self-verified: cargo build --workspace, cargo test --workspace (all green incl. the LongOnly node tests, the member_key worked-examples corpus, the varying_axes accessor test, and the updated SMA sweep-trace integration test), cargo clippy --workspace --all-targets -D warnings. refs #105 |
||
|
|
d3cb5f8052 |
feat(family-traces): persist per-member streams for sweep/MC/walk-forward
Family runs (sweep, Monte-Carlo, walk-forward) ran a full harness per
member and drained its equity+exposure recorders, but immediately folded
the streams to metrics and discarded them — so the C21 family-comparison
view had no per-member data to chart. A new opt-in `--trace <name>` on
`aura sweep|mc|walkforward` now persists each member's streams to disk,
mirroring the single-run `aura run --trace` path. Any single member is
chartable today via `aura chart <name>/<member_key>`; the comparison view
itself is a later cycle.
Design:
- Persist INSIDE each per-member closure (aura-cli), reusing persist_traces
verbatim — the engine and the family types are untouched. The closures
already held the drained rows; they now bind them once, build the
manifest, persist when --trace is set, then fold as before.
- Content-derived, deterministic member keys (MC seed{N}, sweep f{fast}s{slow},
walk-forward oos{ns}). The engine HOFs are Fn + Sync — members run in
parallel, so a runtime ordinal counter would be non-deterministic (a C1
break); keys derive from what each closure receives deterministically.
sweep_member_key panics (naming the axis) if the grid lacks a key axis,
rather than silently collapsing two points to a shared dir.
- Each member is a standalone run-dir at runs/traces/<name>/<member_key>/
(persist_traces with a slash-name; TraceStore nests via create_dir_all),
so the existing single-run viewer charts a member with no view-side code.
- Opt-in: without --trace, stdout and the run registry are byte-unchanged;
the cardinality cost of N members x full resolution stays user-chosen.
Out of scope (deferred): the family-comparison view (overlay/small-multiples),
decimation/LOD, a binary trace container.
Verified locally: cargo build --workspace, cargo test --workspace (all green,
incl. 3 new family persistence tests + a sweep determinism test + 2
sweep_member_key unit tests), cargo clippy --workspace --all-targets
-D warnings (clean). The pre-existing family determinism and single-run
trace tests pass unchanged (no-trace path is byte-identical).
closes #104
|
||
|
|
06fdafa925 |
feat(trace-charts): keyboard chart control — pan / zoom-toward-cursor / reset / toggle
Keyboard control for the served chart, deliberately the discrete counterpart to the
glitchy hand-rolled pointer drag that was just removed: each key is one clean
setScale("x", …), deterministic, with none of uPlot's pointer-pan jank.
- arrows pan (15% of the visible span), +/- zoom, 0/Home reset, x toggles the spine.
- zoom anchors on the CURSOR: the x-value under the mouse keeps its relative position,
so the point under the pointer stays put while zooming (centre when the mouse is off
the chart). The mouse position is tracked per chart via posToVal on mousemove.
- keys drive every chart instance together, so panels stay x-aligned.
- uPlot's default mouse (drag = box-zoom, double-click = reset) is retained — you have
both.
The load-bearing math is the pure keyXRange(key, lo, hi, anchor, ext) helper (the rest
is browser-only DOM wiring in mount); it is node-guarded by chart_viewer_keys.mjs, which
pins the zoom-toward-cursor invariant (anchor keeps its relative position) with an
off-centre anchor so a centre-zoom regression fails loudly. All five chart guards green.
refs #103
|
||
|
|
08a272c73d |
fix(trace-charts): drop glitchy custom wheel/pan, restore uPlot default mouse control
The hand-rolled wheel-zoom + drag-pan (wheelZoomPlugin) was hakelig/glitchy — uPlot is not built to be driven that way. Remove the plugin and the cursor.drag override so uPlot's own defaults govern the mouse: drag = box-zoom on x, double-click = reset to full range, with the legend value-readout and crosshair unchanged. Everything else from the Tier-1 pass stays: the ordinal (gap-collapsed) x-axis default + continuous toggle, the ordinal-label honesty (ticks/legend report the real xs[i]), and panel cursor-sync (cursor.sync key retained; only the drag override and the plugin are dropped). The x-mode guard is flipped to pin the revert — overlay carries no custom plugin and no cursor.drag override, panels carry no plugin — and stays green alongside the other three chart guards. refs #103 |
||
|
|
45cc1c44c9 |
feat(trace-charts): ordinal x-axis default + zoom/pan/cursor-sync (viewer Tier-1)
The served trace chart's x-axis no longer plots against raw timestamps by default — non-trading gaps (nights, weekends) left horizontal blanks that dominated the view. The x spine is now ordinal by default: every recorded slot sits at an even position, so gaps collapse and the signal fills the width. A header toggle flips to continuous (real-time-proportional) spacing and back; default ordinal honours the requested "x-axis should not be continuous", with continuous as the one-click option. Comfort, all on the read/render side (the engine and the Rust serve contract are untouched — window.AURA_TRACES still carries the real xs, the ordinal indices are derived client-side): - ordinal mode keeps the axis honest: ticks and the legend value-readout map a collapsed index back to the real xs[i], never the index itself. - wheel-zoom (x, cursor-anchored), drag-pan, double-click-reset on the plot (uPlot's default drag-box-zoom is disabled so drag pans). - panels share a cursor.sync key, so the crosshair lines up across stacked panels. Confined to chart-viewer.js (buildCharts gains an xMode param + a wheelZoomPlugin factory; mount re-renders on the toggle) and render.rs (CHART_HEAD toggle button + a chart-only style). The two twin node guards (build/gaps) re-point to the ordinal default in lockstep; a new x-mode guard pins ordinal-default / continuous / panel-sync / plugin wiring, and a label-honesty guard (gappy-traces fixture, xs far from the index spine) pins that ordinal ticks report the real timestamp. Verified: cargo test --workspace green (incl. the four chart guards + render inline tests), clippy --all-targets -D warnings clean, e2e (aura run --trace + aura chart) emits a self-contained page defaulting to ordinal with the toggle. Spec docs/specs/0057, plan docs/plans/0057 (boss-signed; grounding-check PASS). Derived-fork decision log in the issue comments. closes #103 |
||
|
|
15ee6d5f4f |
feat(trace-charts): serve persisted traces as a web chart (iter 2)
Iteration 2 (serve half) of the visual face — spec 0056 / plan 0056:
read a persisted run's traces and serve them as a self-contained uPlot
HTML chart. Completes the goal "tap data from a graph, serve it as a
chart in the browser".
- chart-viewer.js (first-party): a pure buildCharts(traces, mode) that
maps the injected window.AURA_TRACES to uPlot configs (no DOM) — overlay
= one plot, every series on its own y-scale over the shared xs; panels =
one plot per series, all on the same timestamp x. A browser-only mount()
instantiates the vendored global uPlot. Guarded by a node .mjs DOM test
(+ a sparse/gaps variant) shelling out exactly like viewer_dot.rs.
- render_chart_html + ChartMode/ChartData/Series (aura-cli/render.rs):
mirrors render_html — self-contained page, uPlot (1.6.32, vendored
|
||
|
|
d534f106da |
chore(aura-cli): vendor uPlot 1.6.32 for the trace-chart viewer
Vendors uPlot.iife.min.js (the IIFE bundle exposing the global `uPlot`) + uPlot.min.css under assets/, pinned at uplot@1.6.32 via unpkg, mirroring the existing Graphviz-WASM/pan-zoom vendoring (checked in verbatim so include_str! + the build stay hermetic/offline, C1/C8). refresh-assets.sh gains the matching pinned-fetch lines. Consumed by iteration 2's render_chart_html (serve half of spec 0056). refs #101 |