Commit Graph

423 Commits

Author SHA1 Message Date
Brummel ffd18b98ea audit(0070): cycle close — lift finalize lifecycle to ledger C8, drop ephemera
Cycle 0070 (streaming sink reductions, #138) close. Durable rationale lifted to
the ledger: C8 now records the end-of-stream `finalize` lifecycle (the second node
phase beside `eval`), its `Harness::run` topo-order epilogue, and its C1/C7/C8
reconciliation — with the open `Recorder`->`Probe` / per-cycle-allocation question
(#77) cross-referenced, since `finalize` is the "non-channel exit" that issue's
accumulate-then-read option named as missing. The per-cycle spec and plan are
git-rm'd (ephemeral, valid only for their cycle).

Audit (architect) found no invariant breakage; noted debt, left as-is: the
two-mode `stage1_r_graph` (reduce vs raw sink wiring) rests on the CLI-seam
equivalence test rather than structural unification, and `SeriesReducer.last_ts`
carries the last *warm* cycle's ts (latent only if `--trace` ever folds).

refs #138
2026-06-25 11:29:51 +02:00
Brummel 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
2026-06-25 11:24:27 +02:00
Brummel cfe7bad0ff feat(0070): streaming sink reductions — finalize hook + folding sinks (#138)
M3 machinery for BLOCKER #138: sink reductions can now fold online instead of
buffering every per-cycle row to an unbounded channel.

- aura-core: additive `Node::finalize` end-of-stream hook (default no-op) +
  `SeriesFold` online accumulator (last / max_drawdown / sign_flips).
- aura-engine: `Harness::run` calls `finalize` once per node in topo order after
  the source loop; `summarize` refactored to drive `SeriesFold` (byte-identical),
  the now-dead `sign0` removed.
- aura-std: `GatedRecorder` (emits only gated rows + the final row on finalize)
  and `SeriesReducer` (folds one f64 series, emits one summary row on finalize),
  both emitting the `Recorder` channel type — no aura-engine dependency.
- Integration tests: folded reductions are byte-for-byte equal to the
  raw-recorder path; the R-reduction's peak memory is O(trades), independent of
  cycle count.

Ratified two off-plan fixes the implementer made in the tests (both plan defects,
not code defects): the GatedRecorder correctly flushes the final non-gated row on
finalize, so the retained count is K_TRADES + 1; and the heterogeneous PM record
needs kind-correct zero scalars (a blanket f64 zero is rejected by the kind-typed
AnyColumn::push).

Wiring these reducers into the stage1-r sweep (the actual O(cycles)->O(trades)
win) follows next. refs #138
2026-06-25 11:00:02 +02:00
Brummel bf886a1386 plan: 0070 streaming sink reductions
Eight-task plan for the M3 fix: SeriesFold + Node::finalize (aura-core),
Harness::run finalize epilogue + summarize refactor (aura-engine), GatedRecorder
+ SeriesReducer folding sinks (aura-std), stage1_r_graph reduce-mode rewiring
(aura-cli), with byte-identity (equivalence) and O(trades) (memory) integration
tests.

refs #138
2026-06-25 10:20:46 +02:00
Brummel 55dee38d22 spec: 0070 streaming sink reductions (boss-signed)
M3 fix for BLOCKER #138: an end-of-stream `finalize` lifecycle on sinks
lets reductions fold online into O(trades)+O(1) state, so full-history
stage1-r sweeps run in bounded memory per member instead of OOM-killing.
Grounding-check PASS; signed autonomously under /boss.

refs #138
2026-06-25 10:01:52 +02:00
Brummel 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
2026-06-25 00:43:56 +02:00
Brummel 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
2026-06-25 00:11:03 +02:00
Brummel 28e0331223 audit(0068): cycle close — lift position-event-derive rationale to ledger, drop ephemera
Architect audit clean (no drift): the derive is the faithful C10 book
first-difference — Close uses the stored book volume (the actual book), never an
exposure delta (the exact inversion of the abandoned 0064 exposure-integral);
post-run reduction, no in-graph node (C14), positional type-erased Scalar reads
(C7), each event's event_ts is its own cycle (C2), the >1-event-per-instant
reversal case is why it is derived not per-eval (C8/C10); aura-engine -> aura-core
only (instrument_id a caller scalar, no InstrumentSpec import).

- Ledger C10: add a "Realization (cycle 0068, #115)" note — derive_position_events
  as the first difference of the executed book, the post-reframe replacement for
  the rolled-back 0064 derive; fixed-fractional / currency / equity-feedback
  sizing and the realistic brokers stay #116.
- Remove the ephemeral spec + plan (cycle closed; rationale in the ledger + git
  history).

Accepted low debt (architect [low]): the stop-then-same-cycle-reopen producer path
emits the same closed&&open shape as a bias-flip reversal and the derive handles it
correctly (it keys on closed/open/direction/size, not exit_reason; unit-covered via
the reversal test), but is not separately asserted end-to-end. Cosmetic; the derive
is producer-agnostic.

refs #115
2026-06-24 20:23:57 +02:00
Brummel 7aaa3e5d5c feat(0068): position-event derive — book first-difference table (#115)
Add `derive_position_events(record, instrument_id) -> Vec<PositionEvent>` to
aura-engine (report.rs), the broker-independent Stage-2 audit layer of C10: the
first difference of the executed book derived from a PositionManagement dense
record. A pure post-run reduction, sibling of summarize_r — same positional,
type-erased Scalar read of the 14-column record, no in-graph node (the hot path
stays domain-free, C14). A reversal emits Close then the opposite open at one
event_ts (close first); a window-end open position emits its open with no
synthetic Close (the table records actual executed events — unlike summarize_r,
which force-closes for the R metric). instrument_id is a caller-supplied scalar
(aura-engine depends only on aura-core; it cannot import InstrumentSpec).

Two r_col indices added (DIRECTION=4, SIZE=10), the lockstep guard in
stage1_r_e2e.rs extended to name-pin `direction`, and an agreement E2E folds both
summarize_r and derive_position_events over one recorded ledger (one Close per
closed round-trip; every open closed-or-open-at-end; referential integrity).

Scope: the derive only. Fixed-fractional / currency / equity-feedback sizing is
#116 (it owns the equity->Sizer z^-1 register per C10) — deferring it here avoids
a circular dependency. Supersedes the rolled-back 0064 exposure-integral derive
(abandoned per #117): this derives from the executed book, never exposure deltas.

Verified by the orchestrator: cargo build + clippy --all-targets -D warnings
clean; full workspace suite 0 failed; aura-engine lib 214 pass (incl. the 6
derive_* unit tests); stage1_r_e2e 11 pass (incl. the new E2E + extended guard).
Fork decisions recorded on #115.

closes #115
2026-06-24 20:19:31 +02:00
Brummel 1647737baa plan: 0068 position-event-derive
Three tasks for spec 0068: (1) derive_position_events + r_col DIRECTION/SIZE
consts + crate-root re-export + report.rs unit tests (reversal, normal lifecycle,
short, window-end open, empty, monotonic position_id); (2) stage1_r_e2e.rs
lockstep-guard direction pin + an agreement E2E folding both summarize_r and
derive_position_events over one recorded ledger; (3) full workspace
build/test/clippy verification. Purely additive — no existing output touched.

refs #115
2026-06-24 19:52:27 +02:00
Brummel ef8537c44f spec: 0068 position-event-derive — book first-difference table (#115) (boss-signed)
Derive the broker-independent position-event table (#114's PositionEvent rows)
from a completed PositionManagement run, as the first difference of the executed
book (deal = target - book - in_flight; in_flight = 0 for instant backtest
fills). A pure post-run reduction `derive_position_events(record, instrument_id)`
in aura-engine, sibling of summarize_r, reading the 14-column PM record
positionally; a reversal emits Close then the opposite open at one event_ts
(close first); a window-end open position emits its open with no synthetic Close.

Supersedes the rolled-back 0064 exposure-integral derive (abandoned per #117):
this derives from the executed book (the PM record), never from exposure deltas.
Stage-2 fixed-fractional / currency / equity-feedback sizing is out of scope
(#116 owns the equity->Sizer z^-1 register per C10).

Boss-signed: specify Step-5 grounding-check PASS (8/8 load-bearing assumptions
ratified by named green tests). Fork decisions recorded on #115.

refs #115
2026-06-24 19:45:31 +02:00
Brummel 458b2ae74b audit(0067): cycle close — lift SQN100 / stage1-r-trace rationale to ledger, drop ephemera
Architect audit clean (no drift, no debt): raw sqn verified bit-identical,
C18 serde back-compat + C4 SoA seam + C1 determinism + Part-B symmetry all hold.

- Ledger C10: add a "Realization (cycle 0067, #130 + #135)" note — SQN100 as
  the turnover-robust opt-in rank key (raw sqn / default ranker byte-unchanged,
  serde(default) for back-compat), and per-member --trace now symmetric across
  all three sweep strategies via the shared persist_traces_r.
- Glossary: R-metrics list gains sqn_normalized; the SQN entry documents the
  SQN100 variant (was "deferred #130").
- Remove the ephemeral spec + plan (cycle closed; rationale lives in the ledger
  and git history).

refs #130, refs #135
2026-06-24 18:15:37 +02:00
Brummel 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
2026-06-24 18:10:54 +02:00
Brummel a726974310 plan: 0067 r-sweep-followups
5-task plan for the boss-signed spec docs/specs/0067-r-sweep-followups.md.
Task 1 adds RMetrics.sqn_normalized (+ SQN_CAP + summarize_r compute) and
threads all four RMetrics struct literals (compile-gate). Task 2 adds the
Metric::SqnNormalized rank key. Task 3 re-baselines the 0066 single-run
golden (the r: block gains the field). Task 4 wires --trace for the stage1-r
sweep (invert the refusal test → persist member dirs via persist_traces_r,
drop the run_sweep guard). Task 5 is the full-workspace gate.

Sequencing pins the golden re-pin and the unfiltered suite after Task 1.

refs #130, refs #135
2026-06-24 17:28:48 +02:00
Brummel e5c87a5173 spec: 0067 r-sweep-followups (boss-signed)
Two settled, additive follow-ups to the cycle-0065/0066 R-sweep surface,
named together by the user under /boss. Signed on the grounding-check PASS
(9 load-bearing current-behaviour assumptions, each ratified by a named
green test).

Part A (refs #130) — n-normalized SQN (SQN100): a new opt-in ranking metric
sqn_normalized = (mean_R/stdev_R)·√(min(n,100)), turnover-robust across sweep
members with different trade counts. Additive RMetrics field (serde(default)
for C18 back-compat) + a new Metric::SqnNormalized rank key mirroring Sqn.
Raw sqn and the default ranker are unchanged; the 0066 single-run golden is
re-baselined (the r: block gains one field — intended, not drift).

Part B (refs #135) — wire --trace for the stage1-r sweep: stage1_r_sweep_family
honours --trace by 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 refusal guard in run_sweep is dropped.
Makes --trace symmetric across all three sweep strategies.

Derived fork decisions logged on #130 and #135 (reconciliation comments).
2026-06-24 17:16:37 +02:00
Brummel 986a9fd5c7 audit(0066): cycle close — lift R-sweep/SQN rationale to ledger, drop ephemera
Cycle 0066 (#133) closed. The close audit (architect) found the family ratified against C12 axis-2 (rank-by-metric over the C18 family store) and the C10 R framing, with one [high] gap (swept-member manifest omitted the fixed R-defining params) fixed forward in 499121e.

- Ledger: a C10 realization note records the durable rationale -- SQN as the single-number sweep objective, the signal-only grid (the stop defines R, so varying it breaks cross-member SQN comparability -- the SQN100 #130 motivation), the R-metric rank vocabulary, and the manifest-completeness fix.
- Glossary: new 'R metrics' and 'SQN' entries.
- The ephemeral spec + plan are removed; the ledger + git history are the durable record.

Follow-ups filed: #135 (per-member --trace for the stage1-r sweep). refs #133
2026-06-24 16:24:25 +02:00
Brummel 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
2026-06-24 16:21:26 +02:00
Brummel 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
2026-06-24 16:15:45 +02:00
Brummel d430dbb885 plan: 0066 r-sweep-rank — R sweep families + rank-by-SQN
Parent spec docs/specs/0066-r-sweep-rank.md (#133). Four tasks: golden characterization of the stage1-r single run (byte-preservation guard); extract stage1_r_graph helper (float fast/slow for the sweep, single run binds Some(2)/Some(4)); metric_cmp learns sqn/expectancy_r/net_expectancy_r (None sorts worst); stage1_r_sweep_family + --strategy stage1-r + rank-by-sqn integration test.

refs #133
2026-06-24 15:30:53 +02:00
Brummel 1c8678a6ad spec: 0066 r-sweep-rank — R sweep families + rank-by-SQN (boss-signed)
Cycle 0066, reference issue #133. Settled design (deferred from 0065): make SQN
the single-number objective for ranking a Stage-1 sweep family by R signal
quality.

Two halves:
- aura-registry: metric_cmp learns sqn / expectancy_r / net_expectancy_r (all
  higher-is-better, reaching into RunReport.metrics.r); a member with r:None sorts
  to the worst position. No persistence change (the family store already carries r).
- aura-cli: --strategy stage1-r sweeps the stage1-r harness over a fast×slow signal
  grid (stop & sizing fixed — risk_budget is R-invariant, bias.scale is sign-only
  for flat-1R, both degenerate axes; the stop defines the R unit so varying it
  breaks cross-member SQN comparability — see the #133 decision log). The stage1-r
  topology is extracted into a shared helper so the single run keeps its identical
  binding (output unchanged by construction) and only the sweep floats fast/slow.

Signed under /boss via the grounding-check PASS (the autonomous signature). The
first grounding pass BLOCKed a false claim that the stage1-r single-run output was
already byte-pinned; the spec was revised so a golden characterization test is
added against current HEAD before the refactor (verifying byte-preservation rather
than assuming it), and re-grounded to PASS.

refs #133 #117
2026-06-24 15:13:56 +02:00
Brummel 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
2026-06-24 14:45:27 +02:00
Brummel 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
2026-06-24 14:40:51 +02:00
Brummel 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
2026-06-24 14:35:51 +02:00
Brummel 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.
2026-06-24 13:57:15 +02:00
Brummel 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).
2026-06-24 13:44:44 +02:00
Brummel c406b969f5 chore(stage1-r): finalize cycle 0065 — milestone closed, ephemeral spec/plans removed
The 'R-based signal quality (Stage 1)' milestone is complete and ratified
(iter-1/2/3 + the corrective primitives mini, the tiny-pip fix, the metric rename;
end-to-end fieldtest green). Closing the cycle:

- Milestone container + issues #119/#126/#127/#128/#129 closed on the tracker.
- The ephemeral per-cycle spec (0065) and plans (0065-0069) are git-rm'd per the
  cycle-close convention; the durable rationale lives in the design ledger (the C10
  cycle-0065 realization note) plus git history.

Follow-ons tracked: #130 (n-normalized SQN), #131 (CLI UX), #132 (harness polish),
#133 (R-sweep + rank-by-sqn), #134 (strategy-output param-namespace rename).

refs #117
2026-06-24 13:13:58 +02:00
Brummel 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
2026-06-24 13:11:46 +02:00
Brummel 34e8bbc803 plan: 0069 exposure->bias rename tail (#126)
The deferred rename of the strategy-output residue (the typed exposure_sign_flips
metric with serde back-compat, the registry rank vocab, the exposure_scale manifest
label, the .named("exposure") Bias instances), keeping SimBroker's pre-reframe
exposure concept and the on-disk exposure tap label (scope fork on #117).

refs #126 #117
2026-06-24 12:45:22 +02:00
Brummel 4e6b1d9b53 audit(stage1-r): cycle-0065 close — ledger records the Stage-1 R signal-quality layer
Architect drift review of cycle 0065 (Stage-1 R: the exposure->bias rename, the
stop-rule + vol_stop, PositionManagement's dense R-record, the Sizer, summarize_r,
the public RiskExecutor, and the CLI surface): code is drift-clean - C10 fidelity
holds (bias->stop->Sizer->PositionManagement, flat-1R feed-forward, R stop-defined
and size-invariant, guarded by the cross-crate layout lockstep test), C9/C23
acyclic, the --harness selector is a compile-time match (no runtime registry/DSL,
C17). The milestone fieldtest is green after the tiny-pip tolerance fix (2ee0d74).

Lifted to the ledger (docs/design/INDEX.md):
- A C10 realization note for cycle 0065 recording the built Stage-1 R layer (the
  chain, the public RiskExecutor + StopRule axis, the CLI surface, the scale-robust
  guard, the aura-engine->aura-std normal dep) and that Stage 2
  (currency/compounding/realistic brokers) stays the deferred downstream layer.
- Amended the C10 contract's "pending Stage-1 implementation" line (it landed).

Fixed stale prose introduced by iter-3's dependency-tier change: report.rs and
position_management.rs comments cited "aura-std is only a dev-dependency" as the
rationale for the by-convention dense-record layout contract; iter-3 made it a
normal dependency, so that rationale was false. Reworded to the real,
dep-tier-independent reason - the record crosses the crate boundary as a
type-erased Scalar vector (C4 SoA), read positionally, guarded by stage1_r_e2e.rs.

Carry-on filed as follow-ons (not actioned): the exposure->bias rename tail (the
serde-stable exposure_sign_flips key + slot/param names - commented on #126, the
recommended next iteration); CLI UX friction (#131); stage1-r harness cosmetic
smells (#132); R-sweep families + rank-by-sqn (#133).

This is a drift-clean cycle-close audit, NOT a milestone-container close: the
ephemeral spec/plans stay (spec 0065's change (a) still scopes the deferred rename
tail) and the milestone close stays a deliberate act for the user.

cargo build/test --workspace green; clippy --workspace --all-targets -D warnings clean.

refs #117 #126
2026-06-24 12:35:15 +02:00
Brummel 2ee0d74a8c fix(stage1-r): scale-robust tolerance for the R-record redundancy guard
The PositionManagement redundancy `debug_assert` (position_management.rs:229)
re-derives realized_r from the dense-record columns as
dir*(exit-entry)/|entry-stop| and compared it to the stored value with an
ABSOLUTE < 1e-9 tolerance. At tiny-pip price scale (EURUSD/GBPUSD entry ~1.1,
vol-stop distance ~1e-6) the denominator reconstruction `entry - stop` is a
catastrophic-cancellation subtraction that loses ~6 significant digits; with a
deep gap-through-stop realising a large |R|, the recomputed R drifts from the
(correct) stored realized_r by more than 1e-9 - so a debug build PANICS (exit
101) while a release build (assert compiled out) returns the correct R. GER40
(large pip) never trips it.

Found by the Stage-1 R milestone fieldtest: `aura run --harness stage1-r --real
EURUSD` crashed the headline "score real FX in R" use case on the default (debug)
build, and the debug/release disagreement made the R numbers' validity
unknowable from the public surface.

Root cause (via debug): the stored realized_r uses the cancellation-free FROZEN
latched_dist and is exact - only the guard's tolerance was wrong for small price
scales. Fix: a scale-robust relative tolerance `< 1e-9 * realized.abs().max(1.0)`
(floored at the historical 1e-9 for |R| <= 1, so no change for normal-scale runs).

RED-first: the hermetic synthetic-tiny-pip test
`tiny_pip_gap_stop_does_not_trip_the_redundancy_guard` reproduces the panic and is
pinned here. Verified end-to-end: `aura run --harness stage1-r --real EURUSD` now
returns the R block (exit 0) on a debug build. cargo test --workspace green;
clippy --workspace --all-targets -D warnings clean.

refs #117
2026-06-24 12:30:08 +02:00
Brummel 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
2026-06-24 12:09:58 +02:00
Brummel 13086b8590 plan: 0068 stage1-r CLI surface (iter-3)
Executable plan for iteration 3 of cycle 0065 - make the Stage-1 R layer
operable from the command line. Three tasks:
- Task 1: promote vol_stop + risk_executor + a StopRule axis from test
  fixtures to public aura-engine composite-builders (aura-std moves to a
  normal dep; the two fixtures rewired to call them; a Vol-backed RED test
  added).
- Task 2: the stage1_r_blueprint dual-tap harness (one bias fanned into both
  SimBroker and the RiskExecutor) + run_stage1_r folding summarize_r into
  RunMetrics.r + persist_traces_r (a separate 3-tap helper that leaves the
  seven pip callers byte-unchanged).
- Task 3: a parse_run_args tokenizer + --harness selector + run_dispatch,
  replacing the run literal-slice arms; binary-driven selector / flag-
  composition / r_equity-round-trip tests.

Recon: two plan-recon agents (aura-cli surface, aura-engine promotion).
Verified before handoff by a 3-critic adversarial pass (API-fidelity,
plan-discipline, behavioral-correctness): discipline and behavior SOUND, one
API fix applied (tempdir -> the existing temp_cwd helper, no tempfile dep).

refs #129 #117
2026-06-24 11:20:44 +02:00
Brummel 08d09df97d spec: 0065 stage1-r iter-3 CLI surface (boss-signed)
Amend the cycle-0065 spec with the "Iteration 3 - the CLI surface" design:
make the Stage-1 R layer operable from the command line (the research loop's
primary verb, ergonomic for Claude to drive), splitting the CLI/recording
surface out of the originally-lumped iter-2.

Design (forks derived under /boss, user-delegated "du entscheidest das Design",
recorded on #117):
- Promote vol_stop + risk_executor from test fixtures to public aura-engine
  composite-builders; the embedded stop generalizes to a StopRule{Fixed,Vol}
  enum (a C11 structural axis), vol_stop the volatility-defined default.
- aura run --harness <name>: a compile-time selector (sma|macd|stage1-r) over
  Rust-authored built-ins - running, not wiring (C9/C17 intact: no runtime
  registry, no DSL). Replaces the run literal-slice arms with a parse_run_args
  tokenizer so --harness/--real/--trace compose orthogonally; --macd kept as an
  alias.
- The stage1-r harness fans one bias into both SimBroker (pip) and the
  RiskExecutor (R), so one RunReport carries both yardsticks honestly; the run
  path folds summarize_r -> RunMetrics.r (additive, skip_serializing_if keeps
  pip-only/legacy JSON byte-unchanged), round_trip_cost=0.0 (Stage-1 is
  frictionless; a --cost knob is a follow-on).
- An r_equity tap (LinComb of cum_realized_r+unrealized_r) persists via an
  extended persist_traces and renders through the existing aura chart --tap -
  no new viewer code.

Scope line: "operable from the CLI" = run+inspect a Rust-authored harness (the
#109 (a) reading the contracts settle), not authoring topology in the CLI; the
broad #109 meta-level stays a separate future milestone. Deferred follow-ons:
R-sweep families + rank-by-sqn, rich R-record charting, the bias_sign_flips
metric rename (never carried out in iter-0; field is still exposure_sign_flips).

Verified before signing: grounding-check PASS (all substrate + post-patch
current-code claims ratified by green tests) - the autonomous /boss signature;
plus an advisory 5-lens spec-skeptic panel (criterion/invariants/scope SOUND;
ambiguity + plan-readiness BLOCKs resolved inline: the argv tokenizer, the
3-tap persist change, the round_trip_cost value, the dual-tap arg provenance),
re-grounded after the patch.

refs #129 #117
2026-06-24 10:56:39 +02:00
Brummel 92591e5dc0 refactor(stage1-r): name the conviction column conviction_at_entry
Rename the dense PositionManagement record's column 9 from `bias_at_entry_abs`
to `conviction_at_entry` (FIELD_NAMES, the `r_col`/test index constants, the
cross-crate layout guard, and the doc comments).

Why: the column holds |bias| at entry, which C10 defines AS conviction (bias =
direction + conviction magnitude; ledger INDEX.md ~458). The record already names
the other half of that decomposition by its domain concept (`direction` = sign,
not `bias_sign`), so naming the magnitude half mechanically (`bias_at_entry_abs`)
was an inconsistency; `conviction_at_entry` speaks the ledger's ubiquitous
language and parallels `direction`, while `_at_entry` preserves the
frozen-snapshot precision.

Considered and rejected: collapsing `(direction, bias_at_entry_abs)` into one
signed `bias_at_entry` and dropping `direction` as redundant — it is not
redundant. On a reversal row col 4 (`direction`) tracks the reopened leg while
col 9 tracks the closed trade's entry conviction, so the two describe different
trades and cannot be losslessly merged; they are also distinct semantic axes
(current-position direction vs per-trade entry conviction).

Scope: domain name at the data boundary, mechanical name internally — the
internal `bias.abs()` variables (`Open.bias_abs`, the fold's `Trade.bias_abs`)
keep their computation-faithful names, exactly the raw-internal / domain-external
split the record uses elsewhere.

Behaviour-preserving: cargo build --workspace clean; cargo test --workspace 500
passed, 0 failed (unchanged); clippy --all-targets -D warnings clean. The layout
guard (`r_col_indices_match_producer_field_layout`) confirms the renamed string.

refs #117
2026-06-24 10:25:18 +02:00
Brummel b4e84335c4 feat(stage1-r): Sizer + RiskExecutor + R-metric enrichment (iter 2)
Iteration 2 of cycle 0065 (Stage-1 R signal quality) — the node + metric layer.
Builds on iter-1's PositionManagement dense R-record + core summarize_r.

What ships:

- PositionManagement gains a 4th `size` input (slot 3 -> col 10), fed by the
  Sizer. R is computed size-INVARIANTLY (pure stop-distance ratio), so size
  never touches realized_r — pinned by a RED test (scaling size leaves every
  realized_r unchanged) at the node and again end-to-end through the executor.

- Sizer (aura-std): `size = risk_budget / stop_distance` — flat-1R (risk_budget
  1.0 => one risk unit per trade, size inversely proportional to the stop, not a
  constant). Reads bias (firing/presence) + stop_distance; the Stage-2
  fixed-fractional sizer slots in unchanged (swap risk_budget for
  risk_fraction*equity, same node shape).

- summarize_r enrichment: SQN (sqrt(n)*mean_R/sample-stdev_R; n<2 or zero-variance
  -> 0), conviction_terciles_r (E[R] by |bias_at_entry| tercile), net_expectancy_r
  (gross minus one round-trip cost per trade, charged in R via the latched_dist
  recovered from the entry_price/stop_price columns). summarize_r gains a
  `round_trip_cost` param (price units). The net-of-cost recovery is tested
  through the real producer->consumer seam, not just hand-built rows.

- RunMetrics.r: Option<RMetrics> with #[serde(default, skip_serializing_if =
  "Option::is_none")] — legacy runs.jsonl (no `r` key) deserialise to None and a
  pip-only run's on-disk shape stays byte-unchanged (C14/C18 back-compat). Every
  RunMetrics literal threaded (report.rs, aura-registry, aura-engine/mc).

- RiskExecutor (aura-engine integration-test fixture, sibling of
  vol_stop_composite): FixedStop -> Sizer -> PositionManagement behind open
  bias+price input roles, price fanned to both the stop and PM, the Sizer's size
  into PM. Bias is produced in-graph from the single price source (a second bias
  *source* would k-way-merge into separate cycles and mark stale prices). The
  Veto is a DOCUMENTED SEAM, not a runtime node (a pass-through identity is what
  C19/C23 DCE deletes). Tests: the composite bootstraps + runs + folds to the
  documented hand value; R invariant under risk_budget while the size column
  scales; a live-folded RMetrics survives the RunMetrics serde round-trip.

The dense-record size column (10) is brought under the cross-crate layout guard
(stage1_r_e2e r_col_indices_match_producer_field_layout) so the executor
fixture's size-invariance read is drift-protected like the others.

Scope: the CLI/recording surface (#129) is sub-split into a separate iteration 3
and is NOT in this commit.

Verified: cargo build --workspace clean; cargo test --workspace 500 passed,
0 failed; cargo clippy --workspace --all-targets -D warnings clean.

refs #117 #127 #128 #129
2026-06-24 02:15:51 +02:00
Brummel 6e214957d1 plan: 0067 stage1-r iter-2 — Sizer + RiskExecutor + metric enrichment
Iteration-2 plan for cycle 0065 (Stage-1 R signal quality). Sub-splits the
spec's iteration 2: this plan is the node + metric layer; the CLI/recording
surface (#129) becomes a separate iteration 3.

Five tasks, sequenced so each signature change threads all its callers before
its build gate, and the two signature changes land before the composite that
consumes both:

1. PositionManagement gains a 4th `size` input (col 10, from the Sizer); R
   stays size-INVARIANT (RED test pins it). Threads the PM unit tests + the
   stage1_r_e2e direct-eval column setups.
2. Sizer node (aura-std): `size = risk_budget / stop_distance`, flat-1R; the
   Stage-2 fixed-fractional seam slots in unchanged.
3. summarize_r enrichment: SQN, conviction terciles, net-of-cost; gains a
   `round_trip_cost` param (reads dense cols 6/7/9). Threads report.rs + the
   E2E callers and extends the cross-crate layout guard.
4. RunMetrics.r: Option<RMetrics> with serde(default, skip_serializing_if) for
   legacy runs.jsonl back-compat. Threads every RunMetrics literal (report.rs,
   aura-registry, aura-engine/mc).
5. RiskExecutor composite (aura-engine integration-test fixture, sibling of
   vol_stop_composite): FixedStop -> Sizer -> PositionManagement behind open
   bias+price roles, price fanned to both; bias produced in-graph (single
   source, no stale-price two-cycle merge). Veto is a documented seam, not a
   node. E2E equivalence + R-invariance under risk_budget.

refs #117
2026-06-24 01:28:03 +02:00
Brummel 0998f9aabf refactor(stage1-r): the vol stop is a composition, not a fused node
Applies the principle the user named: a node is a primitive only if it is NOT
DAG-expressible from other primitives; a function that needs a missing primitive
gets the primitive added, not fused away.

- Remove the fused VolStop node (it was pure feed-forward arithmetic).
- The volatility stop is now a composition of primitives, vol_stop(length, k):
  k * Sqrt(Ema(Mul(d,d), length)), d = Sub(price, Delay(price,1)) -- a rolling
  EWMA standard deviation. Built with GraphBuilder; proven end-to-end
  (bootstraps + runs + emits k*sigma) in tests/vol_stop_composite.rs.
- Migrate the one VolStop caller (stage1_r_e2e.rs) off it: the R-is-stop-defined
  test now contrasts a tight vs a wide FixedStop (two constant distances still
  fold to different R, the property under test).

Uses the Mul + Sqrt primitives added in the prior commit. FixedStop stays the
only stop-rule primitive (a triggered constant). Full suite + clippy green.

refs #117 #119
2026-06-24 01:04:19 +02:00
Brummel 831092841e feat(aura-std): add Mul + Sqrt primitives
The two genuinely-missing arithmetic primitives (Mul = two-stream f64 product,
Sqrt = one-input f64 root, negatives clamped to 0). They are the building blocks
for the volatility stop as a composition (rolling EWMA stddev), replacing the
fused VolStop node. Also corrects plan 0066 Task 3 (the VolStop removal must
migrate its stage1_r_e2e.rs caller — a false premise the implementer caught).

refs #117 #119
2026-06-24 00:53:28 +02:00
Brummel 5de8576fd0 plan: 0066 stage1-r primitives + spec correction (VolStop -> composition)
VolStop was a design error (a fused node justified by a missing primitive). A
node is a primitive only if not DAG-expressible from other primitives; a missing
primitive is added, not fused away. Adds Mul + Sqrt primitives, removes the fused
VolStop, expresses the vol stop as a composition (rolling EWMA stddev = k *
Sqrt(Ema(Mul(d,d), length))). Spec 0065 b gets a correction note; full record on #117.

refs #117 #119
2026-06-24 00:37:53 +02:00
Brummel 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
2026-06-23 19:48:58 +02:00
Brummel c5f5e17297 plan: 0065 stage1-r-signal-quality 2026-06-23 17:51:47 +02:00
Brummel 51c87e5a91 spec: 0065 stage1-r-signal-quality (boss-signed)
Stage-1 "R-based signal quality" cycle spec (milestone #119/#126/#127/#128/#129,
contract C10). Signal quality measured in R on a strategy's unsized bias stream,
feed-forward (no equity feedback): bias -> stop-rule -> position-management ->
per-trade R-outcomes -> summarize_r fold -> E[R]/SQN.

Design adversarially hardened (12-juror refute panel; decisions logged on #117):
- R is stop-defined and LATCHED at entry (frozen R-denominator, never re-read).
- Stop-outs are NOT capped at -1R (honest loss tail; gap-through -> R < -1).
- Fills one-cycle-lagged (SimBroker no-look-ahead discipline; RED test mandatory).
- First stop is volatility-scaled (VolStop = k * EMA(|d price|)), not constant
  (a constant makes R just rescaled pips).
- position-management emits a dense per-cycle R-record (trade ledger + R-equity +
  window-end handling), tapped by a Recorder, folded post-run by summarize_r
  (a post-run fold, NOT an in-graph node).
- Sizer = load-bearing flat-1R seam; R computed size-invariantly. Veto = doc seam,
  not a runtime node (DCE deletes an identity).
- exposure -> bias rename lands first (compiler-driven).

Grounding-check PASS (11 assumptions ratified by green tests; auto-signed under /boss).

refs #117
2026-06-23 17:32:05 +02:00
Brummel f040b66f30 design(c10): reframe to a bias stream + R-unit signal quality
Adopt the R-reframe ratified in #117: the strategy's native unit is risk
(R), not pips/dollars, and its primary output is a directional bias.

- exposure -> bias: the primary output is an UNSIZED direction + conviction
  `f64 in [-1,+1]`; sizing and the protective stop leave the strategy.
- Signal quality is measured in R (Stage 1): an R-evaluator integrates the
  per-trade R-outcomes of a flat-1R RiskExecutor into R-expectancy. R is
  defined by the stop, account-/instrument-agnostic; pips retired as the unit.
  Flat-1R needs no equity, so Stage 1 is feed-forward (no cycle).
- Risk-based execution is a decoupled layer: a RiskExecutor composite
  (stop-rule -> Sizer -> Veto -> position-management) per symbol, nested in a
  Broker/Account composite; account mode (netting/hedging) = composition
  constraint. "Sizer" is not "risk-manager" (that names the Veto layer).
- Currency P&L is Stage 2 (fixed-fractional, compounding). The only feedback
  (equity -> Sizer) is cut by a z^-1 register on the fill edge (mark-to-market
  stays a same-cycle price read), encapsulated in the executor; flat-1R vs
  compounding is a structural axis (the explicit register is mandatory because
  C23 reorders the flat graph).
- Position-event table stays the decoupled Stage-2 audit layer = first
  difference of the book (deal = target - book - in_flight); the flawed 0064
  exposure-integral derive is abandoned.

Touches: ledger C10 + CLAUDE.md domain invariant #7 + glossary (adds bias, R,
R-evaluator, RiskExecutor, Sizer, veto, Stage 1/2; revises broker, exposure
stream, position table, sim-optimal broker, strategy, ...). Also fixes
cross-file drift surfaced by adversarial review (C20 guarantee + front-matter
provenance still claimed "exposure stream"; cross-refs corrected to
C9/C19/C20/C23). Industry-grounded against LEAN / nautilus_trader / backtrader
/ QSTrader / vectorbt / zipline.

refs #117
2026-06-23 13:14:31 +02:00
Brummel 2e0500dedd docs(claude): permit the /boss rollback sandbox in the main-sacrosanct rule
The autonomous orchestrator may now git reset --hard its OWN unpushed commits made
this run (above the session anchor) when a line of work dead-ends; pushed and
user-ratified history stays strictly forward-only (git revert, never reset). This
codifies the /boss rollback sandbox the skill assumed but the project rule forbade,
resolving the conflict the auto-mode classifier surfaced. User-authorized.
2026-06-22 22:14:59 +02:00
Brummel 7d7d1f72d9 audit(broker-foundation): cycle close 0063 — ledger records the C10 A-side foundation (#113 + #114)
Cycle-close tidy for broker-foundation (InstrumentSpec deploy metadata #113 +
PositionEvent schema #114).

Architect drift review (3638d48..HEAD): code is drift-clean. PositionEvent matches
the C10 column spec field-for-field (no open_ts, unsigned volume, direction-is-action,
bare-i64 action); the engine run-loop is untouched (C14); InstrumentSpec stays Copy
metadata at the ingestion edge (C7/C15); no instrument identity reaches the hot path.
The only drift was stale ledger prose, fixed here:
- C10 realization note: added a cycle-0063 entry recording that the position-event
  *schema* landed (PositionAction/PositionEvent), while derivation (#115) and the
  realistic broker nodes (#116) remain the deferred decoupled layer.
- C15 realization note: replaced "minimal today (pip only)" with the six-field
  deploy-grade floor and the #124 currency-type forward note (quote_currency is
  &'static str; the #124 resolver chooses how its runtime tier carries currency).

Regression (verbatim): cargo test --workspace = 458 passed / 0 failed (exit 0);
cargo clippy --workspace --all-targets -- -D warnings = clean (exit 0).

Carry-on (not actioned, justified): the inline reversal test is a shape-pin only —
the behaviour test arrives with #115's derive, and the E2E twin already exercises the
reversal wire shape. The quote_currency &'static str / #124 resolver concern is logged
on #124 as a deliberate deferred design choice.

This is drift-clean, NOT a milestone close — the realistic-broker milestone needs its
end-to-end fieldtest green, which depends on #115/#116/#120-123 still to land.

Removed the ephemeral cycle artifacts (spec + plan) per the lifecycle convention.
2026-06-22 19:56:18 +02:00
Brummel b91c89f964 feat(broker-foundation): InstrumentSpec deploy metadata + PositionEvent schema
The realistic-broker milestone's (C10 A-side) two foundation value types — no
broker, no derivation yet, fully tested.

#113 — InstrumentSpec gains six deploy-grade fields (instrument_id,
contract_size, pip_value_per_lot, min_lot, lot_step, quote_currency) and the
vetted table is populated for GER40/FRA40/EURUSD/GBPUSD/USDCAD. The struct stays
Copy (quote_currency is &'static str); the _ => None refuse-don't-guess arm is the
permanent floor. This is the milestone's permanent authored floor (recorded-metadata
tier is forward work, #124).

#114 — PositionAction { Buy, Sell, Close } (closed enum, serde-encoded as a bare
i64 0/1/2 via From/TryFrom) + PositionEvent in aura-engine beside RunMetrics,
faithful to the C10 column spec: event_ts, action, position_id, instrument_id,
unsigned volume; no open_ts; direction is the action (no signed-volume trick).
Re-exported from the crate root.

Derived fork decisions (recorded on #113): quote_currency &'static str; action as
ordinal i64; stable instrument_id + overridable vetted values.

Verified by the orchestrator: cargo test --workspace = 458 passed / 0 failed;
clippy --all-targets -D warnings clean. An adversarial 4-lens review (ledger
faithfulness, downstream soundness, serde/wire robustness, test honesty) returned
3 SOUND + 1 CONCERN: quote_currency &'static str cannot hold a runtime-sourced
currency at the #124 resolver — not a blocker (#115/#116 read only the authored
floor), logged on #124 as a deliberate deferred design choice. The implementer's
deviation from the plan snippet (vec![..] -> [..]) avoids clippy::useless_vec;
semantics identical.

Two tester-authored consumer-boundary E2E tests added beyond the inline units:
instrument_spec_deploy_metadata.rs (public resolution seam) and
position_event_table.rs (persisted bare-int wire shape + reversal table).

closes #113 #114
2026-06-22 19:51:17 +02:00
Brummel 099aac87e1 plan: 0063 broker-foundation — InstrumentSpec deploy metadata + PositionEvent schema
Three tasks. Task 1 (#113, compile-atomic): RED-rewrite the lookup test to the
six-field shape + invariant/uniqueness tests, then extend InstrumentSpec and
populate the vetted table for the five instruments (refuse arm stays). Task 2
(#114, greenfield): RED the round-trip / try_from-Err / serde-bare-int / reversal
tests, then add PositionAction (serde-as-i64) + PositionEvent in report.rs and
re-export from the crate root. Task 3: workspace test + clippy gate.

Recon-grounded line anchors; the redundant std::convert::TryFrom import dropped
(2021 prelude). INDEX.md realization-note drift (C10/C15 "minimal today") deferred
to the cycle-close audit, not folded here (code-only scope).

refs #113 #114
2026-06-22 19:22:42 +02:00
Brummel cc0dce19a3 spec: 0063 broker-foundation — InstrumentSpec deploy metadata + PositionEvent schema (boss-signed)
The broker milestone's (C10 A-side) foundation: two pure value-type
deliverables, no broker and no derivation yet.

- #113: extend InstrumentSpec with deploy-grade fields (instrument_id,
  contract_size, pip_value_per_lot, min_lot, lot_step, quote_currency) +
  populate the vetted table for the five instruments; refuse-don't-guess
  arm stays. Permanent authored floor of the override hierarchy (#124).
- #114: PositionAction { Buy, Sell, Close } (closed enum, serde-encoded as
  i64) + PositionEvent row in aura-engine, faithful to the C10 column spec
  (no open_ts, unsigned volume, direction is the action).

Signed under /boss on a grounding-check PASS (all current-state assumptions
ratified by green tests; the two new types are greenfield, expected).
Derived fork decisions (quote_currency &'static str; action enum encoded as
i64 0/1/2; stable instrument_id + overridable vetted values) recorded on #113.

refs #113 #114
2026-06-22 19:14:07 +02:00
Brummel 3638d4822e docs(ledger): record the tap-aware decimation refinement (#111) in the cut-2 note
The cut-2 amendment described decimate as a pure min/max transform; #111 made the
per-bucket reduction tap-aware (mean for the bounded exposure, min/max for equity).
Keep the always-loaded design memory accurate; note #112 (bars/OHLC rendering) and
#110 as the remaining deferred refinements.

refs #111 #112
2026-06-22 14:35:10 +02:00
Brummel 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
2026-06-22 14:34:21 +02:00