Ledger + glossary: C10 R-reframe (bias / R / RiskExecutor / Stage-1·2) #117

Closed
opened 2026-06-22 16:31:05 +02:00 by Brummel · 11 comments
Owner

Record the C10 A-side realization in the design ledger (docs/design/INDEX.md, under C10's realization notes) and the glossary (docs/glossary.md). The load-bearing part is the Architecture-A clarification of a real over-specification in C10 as originally written:

  • C10 as written required all three of: realistic broker is an "ordinary node", it "attaches on the derived position-event table", and the table is "computed post-run". These three cannot co-hold without a two-pass run + a multi-column / multi-row-per-event_ts Source replay (which does not exist and carries a reversal determinism hole).
  • Resolution (settled): the realistic broker attaches as a single-pass node on the exposure stream, charging friction on exposure deltas; the position-event table is the decoupled post-run AUDIT view, not the broker's input. Brokers stay ordinary nodes. Rejected: two-pass table-as-Source (over-machinery), post-run fold (gives up "ordinary node").
  • Also record: stops/risk are exposure-layer (decision node), the broker owns only stop-fill realism via slippage.

Glossary: revise realistic broker (single-pass node on exposure) and position-event table (decoupled audit view); add currency equity, net_currency_pnl, n_trades.

depends on: the implementation issues (done at cycle close / audit)
context: a contract reinterpretation belongs in the ledger, not silently in code.

Record the C10 A-side realization in the design ledger (docs/design/INDEX.md, under C10's realization notes) and the glossary (docs/glossary.md). The load-bearing part is the **Architecture-A clarification** of a real over-specification in C10 as originally written: - C10 as written required all three of: realistic broker is an "ordinary node", it "attaches on the derived position-event table", and the table is "computed post-run". These three cannot co-hold without a two-pass run + a multi-column / multi-row-per-`event_ts` Source replay (which does not exist and carries a reversal determinism hole). - **Resolution (settled):** the realistic broker attaches as a **single-pass node on the exposure stream**, charging friction on exposure deltas; the position-event table is the **decoupled post-run AUDIT view**, not the broker's input. Brokers stay ordinary nodes. Rejected: two-pass table-as-Source (over-machinery), post-run fold (gives up "ordinary node"). - Also record: stops/risk are exposure-layer (decision node), the broker owns only stop-fill realism via `slippage`. Glossary: revise `realistic broker` (single-pass node on exposure) and `position-event table` (decoupled audit view); add `currency equity`, `net_currency_pnl`, `n_trades`. depends on: the implementation issues (done at cycle close / audit) context: a contract reinterpretation belongs in the ledger, not silently in code.
Brummel added this to the Realistic broker & position-event table (C10 A-side) milestone 2026-06-22 16:31:05 +02:00
Author
Owner

Reframe — this issue's original scope is superseded and repurposed (boss run, 2026-06-23).

The Architecture-A clarification in the body above is no longer the C10 direction. Following an in-context design review — industry-grounded against LEAN, nautilus_trader, backtrader, QSTrader, vectorbt and zipline — the user ratified a deeper reframe of C10. This issue now tracks the C10 R-reframe amendment (design ledger + CLAUDE.md domain-invariant #7 + glossary), and serves as this run's reference issue / decision log.

Run anchor: main HEAD 2e0500d (session anchor for the boss rollback sandbox).

Fork decisions (decision log)

  1. R adopted as the native unit. The strategy answers "how much R out per 1R risked?"; R is defined by the stop. R replaces pips as the account/instrument-agnostic signal-quality yardstick. Rationale: pips are not risk-normalized, R is. Grounded: R-multiple / expectancy is a recognized standard pattern (Van Tharp), decoupled from currency P&L.

  2. exposure → bias. The strategy's primary output is renamed and re-semantized from a signed fractional position f64 ∈ [-1,+1] (which conflated direction + conviction + size) to a bias = direction + conviction only, explicitly unsized. Sizing leaves the strategy. Grounded: LEAN Insight, backtrader Sizer, QSTrader/zipline all separate an unsized directional signal from downstream sizing (4 of 5; nautilus fuses them — so not unanimous, and we sit on the LEAN side, the right one for signal-quality research).

  3. Risk-based execution layer = a RiskExecutor composite. bias → stop-rule → Sizer (in R) → Veto/gate → position-management, per symbol; nested inside a Broker/Account composite that aggregates per-symbol P&L → account equity. Naming: the sizing node is the Sizer, NOT a "risk-manager" — in LEAN/nautilus "risk management/engine" denotes a pre-trade veto layer, which we keep as a distinct Veto seam. The size / veto / fill roles stay distinct inside the composite.

  4. Stage-1 / Stage-2 cleave. Stage 1 (R / signal quality): flat-1R sizing → feed-forward, no equity feedback → R-expectancy; the primary research loop. Stage 2 (currency / deploy viability): fixed-fractional sizing reads equity (compounding) → realistic brokers, real frictions → currency equity. The cleave itself is our own synthesis (closest precedent: vectorbt's vectorized-signal vs sequential-money-management split) — owned, not claimed as industry-standard.

  5. The only feedback is the equity→Sizer cycle, cut by a z⁻¹ register on the FILL edge (mark-to-market stays a same-cycle price read, C2). Encapsulated inside the executor composite. Flat-1R DCEs the register away (Stage-1 feed-forward); compounding keeps it live (Stage-2). Flat-1R vs compounding is a structural axis (C11: instantiating the feedback edge is topology, not a sweep param). The explicit register is required because C11 reorders the flat graph — the implicit read-before-fill ordering by which every surveyed framework cuts the loop would not survive CSE/DCE.

  6. Position-event table stays the decoupled, derived, Stage-2 audit layer — now the first difference of the book via book-tracking (deal = target − book − in_flight), never a per-eval output (C8). The flawed 0064 exposure-integral derive is abandoned.

  7. Account mode (netting/hedging) = composition constraint. Hedging (EU CFD default): many RiskExecutors per symbol, even opposite. Netting: ≤1 per (strategy, symbol); multi-symbol strategies work on netting.

Milestone reconciliation (ratified)

The existing "Realistic broker & position-event table (C10 A-side)" milestone is resequenced as Stage 2, not discarded. A new Stage-1 "R-based signal quality" milestone is inserted before it. #113 / #114 (foundation, committed locally, unpushed) survive for Stage 2; #115 (rolled-back 0064) and #116 to be rewritten to the new taxonomy; #119 (stops) promoted to foundational (the stop defines R). Tracked in follow-up tracker edits this run.

**Reframe — this issue's original scope is superseded and repurposed (boss run, 2026-06-23).** The Architecture-A clarification in the body above is no longer the C10 direction. Following an in-context design review — industry-grounded against LEAN, nautilus_trader, backtrader, QSTrader, vectorbt and zipline — the user ratified a deeper reframe of C10. This issue now tracks the **C10 R-reframe amendment** (design ledger + `CLAUDE.md` domain-invariant #7 + glossary), and serves as this run's reference issue / decision log. **Run anchor:** main HEAD `2e0500d` (session anchor for the boss rollback sandbox). ## Fork decisions (decision log) 1. **R adopted as the native unit.** The strategy answers *"how much R out per 1R risked?"*; R is defined by the stop. R replaces pips as the account/instrument-agnostic signal-quality yardstick. Rationale: pips are not risk-normalized, R is. Grounded: R-multiple / expectancy is a recognized standard pattern (Van Tharp), decoupled from currency P&L. 2. **exposure → bias.** The strategy's primary output is renamed and re-semantized from a signed fractional *position* `f64 ∈ [-1,+1]` (which conflated direction + conviction + size) to a **bias** = direction + conviction only, explicitly **unsized**. Sizing leaves the strategy. Grounded: LEAN Insight, backtrader Sizer, QSTrader/zipline all separate an unsized directional signal from downstream sizing (4 of 5; nautilus fuses them — so not unanimous, and we sit on the LEAN side, the right one for signal-quality research). 3. **Risk-based execution layer = a RiskExecutor composite.** `bias → stop-rule → Sizer (in R) → Veto/gate → position-management`, per symbol; nested inside a Broker/Account composite that aggregates per-symbol P&L → account equity. Naming: the sizing node is the **Sizer**, NOT a "risk-manager" — in LEAN/nautilus "risk management/engine" denotes a pre-trade *veto* layer, which we keep as a distinct **Veto seam**. The size / veto / fill roles stay distinct inside the composite. 4. **Stage-1 / Stage-2 cleave.** Stage 1 (R / signal quality): flat-1R sizing → feed-forward, no equity feedback → R-expectancy; the primary research loop. Stage 2 (currency / deploy viability): fixed-fractional sizing reads equity (compounding) → realistic brokers, real frictions → currency equity. The cleave itself is our own synthesis (closest precedent: vectorbt's vectorized-signal vs sequential-money-management split) — owned, not claimed as industry-standard. 5. **The only feedback is the equity→Sizer cycle, cut by a `z⁻¹` register on the FILL edge** (mark-to-market stays a same-cycle price read, C2). Encapsulated inside the executor composite. Flat-1R DCEs the register away (Stage-1 feed-forward); compounding keeps it live (Stage-2). Flat-1R vs compounding is a **structural axis** (C11: instantiating the feedback edge is topology, not a sweep param). The explicit register is required because C11 reorders the flat graph — the implicit read-before-fill ordering by which every surveyed framework cuts the loop would not survive CSE/DCE. 6. **Position-event table** stays the decoupled, derived, Stage-2 audit layer — now the first difference of the *book* via book-tracking (`deal = target − book − in_flight`), never a per-`eval` output (C8). The flawed 0064 exposure-integral derive is abandoned. 7. **Account mode (netting/hedging) = composition constraint.** Hedging (EU CFD default): many RiskExecutors per symbol, even opposite. Netting: ≤1 per (strategy, symbol); multi-symbol strategies work on netting. ## Milestone reconciliation (ratified) The existing "Realistic broker & position-event table (C10 A-side)" milestone is **resequenced as Stage 2**, not discarded. A new Stage-1 "R-based signal quality" milestone is inserted before it. #113 / #114 (foundation, committed locally, unpushed) survive for Stage 2; #115 (rolled-back 0064) and #116 to be rewritten to the new taxonomy; #119 (stops) promoted to foundational (the stop defines R). Tracked in follow-up tracker edits this run.
Brummel changed title from Ledger + glossary: C10 Architecture-A clarification (single-pass broker, table = audit view) to Ledger + glossary: C10 R-reframe (bias / R / RiskExecutor / Stage-1·2) 2026-06-23 12:58:25 +02:00
Author
Owner

Stage-1 kickoff handoff (fresh-session start point).

The C10 R-reframe is recorded (ledger C10 + CLAUDE.md #7 + glossary, commit f040b66) and the tracker is restructured. A fresh session resumes the Stage-1 work here.

Queue: milestone "R-based signal quality (Stage 1)"

  • #119 stop-rule + R definition (foundational)
  • #126 exposure → bias rename
  • #127 flat-1R Sizer + position-management
  • #128 RiskExecutor composite + Veto seam
  • #129 R-evaluator

Contract: ledger C10. Stage 2 (realistic broker, currency) is the existing milestone, sequenced after, entered only once E[R] > 0.

Suggested first cycle: a brainstorm of the Stage-1 node interfaces — the open design questions the reframe deliberately left to node-level design:

  1. stop-rule — how is the protective stop expressed? (fixed distance / ATR-multiple / structural / pluggable). It defines 1R, so this is the keystone (#119).
  2. per-trade R-outcome record — the schema the position-management node emits per closed trade (entry, stop, exit, realized R), the R-evaluator's input. Scalar-column form (C7)?
  3. position-management tracking — a trade's lifecycle (open at entry, 1R fixed, close at stop/exit) as a stateful node (C9 register) turned into the R-outcome; feed-forward in Stage 1 (no equity read).
  4. Sizer API (flat-1R) — size is trivially "1R" in Stage 1; pin the interface so Stage-2 fixed-fractional slots in without an interface change (the flat-1R/compounding structural axis).
  5. R-evaluator aggregation — per-trade R → R-expectancy / R-curve; what it records to a sink (C18) for the playground.
  6. RiskExecutor composite wiring + where the Veto stub sits (reserved seam; likely a pass-through node in Stage 1).
  7. bias rename coordination (#126) — the Exposure → Bias rename is behaviour-preserving (compiler-driven-eligible) but best landed at the head of this cycle, coordinated with the bias node's consumer (the Sizer).

State: main HEAD f040b66, working tree clean, nothing pushed. The prior run's session anchor was 2e0500d; the reframe decision log is the comment above.

**Stage-1 kickoff handoff (fresh-session start point).** The C10 R-reframe is recorded (ledger C10 + `CLAUDE.md` #7 + glossary, commit `f040b66`) and the tracker is restructured. A fresh session resumes the Stage-1 work here. **Queue:** milestone *"R-based signal quality (Stage 1)"* — - #119 stop-rule + R definition (foundational) - #126 exposure → bias rename - #127 flat-1R Sizer + position-management - #128 RiskExecutor composite + Veto seam - #129 R-evaluator Contract: ledger **C10**. Stage 2 (realistic broker, currency) is the existing milestone, sequenced after, entered only once `E[R] > 0`. **Suggested first cycle: a `brainstorm` of the Stage-1 node interfaces** — the open design questions the reframe deliberately left to node-level design: 1. **stop-rule** — how is the protective stop expressed? (fixed distance / ATR-multiple / structural / pluggable). It defines 1R, so this is the keystone (#119). 2. **per-trade R-outcome record** — the schema the position-management node emits per closed trade (entry, stop, exit, realized R), the R-evaluator's input. Scalar-column form (C7)? 3. **position-management tracking** — a trade's lifecycle (open at entry, 1R fixed, close at stop/exit) as a stateful node (C9 register) turned into the R-outcome; feed-forward in Stage 1 (no equity read). 4. **Sizer API (flat-1R)** — size is trivially "1R" in Stage 1; pin the interface so Stage-2 fixed-fractional slots in without an interface change (the flat-1R/compounding structural axis). 5. **R-evaluator aggregation** — per-trade R → R-expectancy / R-curve; what it records to a sink (C18) for the playground. 6. **RiskExecutor composite wiring** + where the **Veto** stub sits (reserved seam; likely a pass-through node in Stage 1). 7. **bias rename coordination (#126)** — the `Exposure → Bias` rename is behaviour-preserving (`compiler-driven`-eligible) but best landed at the head of this cycle, coordinated with the bias node's consumer (the Sizer). **State:** main HEAD `f040b66`, working tree clean, nothing pushed. The prior run's session anchor was `2e0500d`; the reframe decision log is the comment above.
Author
Owner

Stage-1 node-interface decisions — locked autonomously (boss), adversarially hardened.

The Stage-1 "R-based signal quality" node interfaces are decided. A 12-juror
adversarial refute-panel ran over the candidate decisions before locking; all 12
returned NEEDS_REFINEMENT
(none sound-as-first-drafted, none fatal), and the
refinements below are folded in. The panel earned its keep — three of these would
have shipped a measurement defect.

The locked design (per fork)

Stop-rule emits a DISTANCE, latched at entry as the frozen R-unit. The stop-rule
node is a pure per-cycle stop_distance: f64 (price units, direction-agnostic).
position-management latches the entry-cycle distance ONCE as the immutable
R-denominator (R = (exit−entry)·dir / latched_distance) and tracks the stop LEVEL
separately. Fixed level for the keystone; trailing (level moves monotonically toward
the position off the live distance) is a designed-for structural axis (C20), not built
yet. Never re-latch the R-unit mid-trade — that was the D1/D2 look-ahead trap.

First concrete stop = a volatility stop, not a constant. A constant distance makes
R = pip_pnl/d, i.e. R-expectancy is just pip-expectancy rescaled — the risk
normalisation that distinguishes R from pips vanishes. So the keystone ships VolStop
= k · EMA_length(|price − z⁻¹ price|) as ONE fused node (no Abs/Mul primitive
exists, so the fusion is mandatory anyway), Kahan-safe like Sma, None until warm.
The volatility that sets 1R is sampled at entry and frozen for the trade (C2-safe,
the analogue of SimBroker's prev_exposure freeze). A trivial FixedStop(distance)
ships alongside as a test fixture and the fixed-vs-vol structural-axis sibling. True
ATR (true-range) is a later overlay — it needs OHLC, unwired today.

Stop-outs are NOT capped at exactly −1R. The single most important correction.
Capping every adverse exit at −1R truncates the loss tail and biases E[R] upward,
precisely on breakout/trend strategies (the repo's only example class). Instead:
realise R = (fill − entry)·dir / latched_distance where the stop fill is no better
than the stop level
(gap-through → R < −1, the honest tail). With close-only data the
stop is tested at the close with a documented optimistic bias (no intrabar
excursion visible); when a resampled high/low is present, test against the adverse
extreme (low for long / high for short) and fill no better than the stop. Frictionless
fill at the stop yields exactly −1R only in the no-gap case.

Fills are one-cycle-lagged (no look-ahead). position-management latches entry_price
and marks every exit (stop / bias-flip / target) against the fill held into the
cycle
(SimBroker's "update state AFTER taking the outcome" discipline), never the
same-cycle price the stop breach is tested against. Encoded as intra-node register
state (survives the C23 flat-graph reorder; it is NOT a graph cycle, so it needs no
back-edge support). A RED no-look-ahead test is mandatory from day one, parallel to
sim_broker_no_lookahead: a bias-flip-exit trade's R must equal the lagged-fill R, and
a stopped trade is exactly −1R independent of fill in the no-gap case.

The per-trade R-outcome is a sparse multi-column record (C8-safe). position-management
returns Some(record) only on a close cycle, None otherwise — a single net position
closes ≤1 trade per cycle (a reversal closes one leg and reopens; the reopen is not an
outcome), the genuine asymmetry vs. the position-event table (which needs >1/instant).
Record columns: r_multiple: f64 (authoritative, debug-asserted against
dir·(exit−entry)/|entry−stop|), exit_reason: i64 (enum: stop / bias-flip /
reversal-leg / window-end), was_stopped: bool (the one attribution prices can't
losslessly reconstruct under gaps), entry_ts/exit_ts: timestamp,
entry_price/stop_price/exit_price: f64, direction: i64 ∈ {−1,+1},
bias_at_entry_abs: f64 (conviction, for the calibration diagnostic), size: f64.

The R-evaluator is a POST-RUN FOLD (summarize_r), not an in-graph node. Recorder
taps are drained post-run (rx.try_iter().collect(); the playground is web-from-disk,
C22 amendment) — nothing consumes a recorder stream mid-run, so an in-graph R-curve node
buys nothing and double-maintains state with a drift risk and a broken (sparse, gapped)
time axis. Stage 1: position-management emits the sparse R-outcomes → a Recorder taps
them → summarize_r (sibling of summarize) folds the trade ledger into the metrics
and a by-trade cumulative-R curve (running partial-sums, labelled by-trade, not
time-drawdown). The in-graph running-equity node is held in reserve for Stage 2, where
compounding genuinely needs it through the z⁻¹ fill-edge register.

The Sizer is a load-bearing seam (size = risk_budget / stop_distance, flat-1R).
"size = 1.0 constant" is constant-notional, not flat-1R; true flat-1R is risk-per-trade
constant ⇒ size varies inversely with the stop. The Stage-1 body emits size = 1 / stop_distance (risk_budget = 1R), so it actually reads its stop_distance input and the
interface is exercised — Stage 2 swaps the constant for risk_fraction · equity, same
shape. R is computed size-invariantly by position-management, so size never
contaminates the R-multiple (the property that makes Stage 1 feed-forward). Two tests:
the flat-1R invariant (size·stop_distance ≡ risk_budget) and R-invariance under size
scaling.

The Veto is OMITTED as a runtime node in Stage 1. A pass-through identity node is
exactly what C19/C23 DCE is licensed to delete; pinning it to survive would be an
implicit-eval-order dependency C23 forbids. It is reserved as a documented seam (a
named position in the spec, not a BlueprintNode), added as a real gating node with a
real edge the cycle it first vetoes.

Conviction: sign-only sizing, but magnitude is measured. flat-1R + sign(bias) entry,
equal-weighted E[R] as the headline (do NOT conviction-weight the fold — that leaks
sizing into the yardstick). But |bias_at_entry| is recorded per trade and summarize_r
reports a conviction-calibration diagnostic (per-tercile E[R] / rank-corr(|bias|,R)),
so a strategy whose edge is conviction-ranking (E[R]≈0 equal-weighted, strong
corr(|bias|,R)) is not mis-reported as worthless. The deferred |bias|>θ threshold falls
out as the 2-bucket read.

The RiskExecutor is a per-symbol Composite, bias+price as input roles (price fanned to
stop-rule and position-management — GraphBuilder::feed supports fan-out), exposing the
R-outcome; summarize_r (the R-evaluator) sits downstream/outside, folding the recorded
outcomes (matches C10 verbatim and the SimBroker-emits / summarize-folds pattern). The
Broker/Account composite (nesting + equity aggregation) is Stage 2, not built now.

Metric set (all new RunMetrics fields #[serde(default)] for runs.jsonl
back-compat): E[R], win_rate, n_trades, avg_win_R, avg_loss_R, profit_factor, max
by-trade R-drawdown, SQN (√n · meanR / stdevR, the dispersion-adjusted single-number
sweep objective), the conviction diagnostic, and a net-of-cost E[R] (one round-trip
spread, a price-unit param — still account-agnostic — so a churny strategy's R can't look
fine while costs would sink it). Open trades at window end are force-closed mark-to-close
with exit_reason = window-end
and their count surfaced — never silently folded as
unrealized MtM (the bug summarize has today).

Note on substrate

This is NEW discrete-trade machinery, not a SimBroker extension: SimBroker integrates a
continuous exposure·return into a pip curve (no trade, no stop, no R) — it is the
ancestor in spirit only. Stage 1 adds a discrete close-detector + stop + entry/exit
pairing producing realised per-trade R.

Iteration cut

  • Commit 0 (head): exposure → bias rename (#126) — behaviour-preserving,
    compiler-driven, its own commit.
  • Iter 1: VolStop + FixedStop (#119) + position-management lifecycle producing the
    size-invariant sparse R-outcome + the entry-latch / lagged-fill discipline + a core
    summarize_r fold (#127 core, #129 core). A complete, runnable, testable R-producing
    vertical slice (source → bias → stop-rule → position-management → Recorder,
    folded by summarize_r); R-invariance is the load-bearing property pinned here.
  • Iter 2: the load-bearing Sizer seam + the RiskExecutor composite (#128, Veto
    documented-not-built) + the enriched summarize_r (SQN, conviction diagnostic,
    net-of-cost, window-end handling) + the new RunMetrics fields + the CLI/recording
    surface (#129).

Proceeding to specify with this as the ratified design.

**Stage-1 node-interface decisions — locked autonomously (boss), adversarially hardened.** The Stage-1 "R-based signal quality" node interfaces are decided. A 12-juror adversarial refute-panel ran over the candidate decisions before locking; **all 12 returned NEEDS_REFINEMENT** (none sound-as-first-drafted, none fatal), and the refinements below are folded in. The panel earned its keep — three of these would have shipped a measurement defect. ### The locked design (per fork) **Stop-rule emits a DISTANCE, latched at entry as the frozen R-unit.** The stop-rule node is a pure per-cycle `stop_distance: f64` (price units, direction-agnostic). position-management latches the *entry-cycle* distance ONCE as the immutable R-denominator (`R = (exit−entry)·dir / latched_distance`) and tracks the stop LEVEL separately. Fixed level for the keystone; trailing (level moves monotonically toward the position off the live distance) is a designed-for structural axis (C20), not built yet. **Never re-latch the R-unit mid-trade** — that was the D1/D2 look-ahead trap. **First concrete stop = a volatility stop, not a constant.** A constant distance makes `R = pip_pnl/d`, i.e. R-expectancy is just pip-expectancy rescaled — the risk normalisation that distinguishes R from pips vanishes. So the keystone ships `VolStop` = `k · EMA_length(|price − z⁻¹ price|)` as ONE fused node (no `Abs`/`Mul` primitive exists, so the fusion is mandatory anyway), Kahan-safe like `Sma`, `None` until warm. The volatility that sets 1R is **sampled at entry and frozen** for the trade (C2-safe, the analogue of SimBroker's `prev_exposure` freeze). A trivial `FixedStop(distance)` ships alongside as a test fixture and the fixed-vs-vol structural-axis sibling. True ATR (true-range) is a later overlay — it needs OHLC, unwired today. **Stop-outs are NOT capped at exactly −1R.** The single most important correction. Capping every adverse exit at −1R truncates the loss tail and biases E[R] upward, precisely on breakout/trend strategies (the repo's only example class). Instead: realise `R = (fill − entry)·dir / latched_distance` where the stop fill is **no better than the stop level** (gap-through → R < −1, the honest tail). With close-only data the stop is tested at the close with a **documented optimistic bias** (no intrabar excursion visible); when a resampled high/low is present, test against the adverse extreme (low for long / high for short) and fill no better than the stop. Frictionless fill at the stop yields exactly −1R only in the no-gap case. **Fills are one-cycle-lagged (no look-ahead).** position-management latches `entry_price` and marks every exit (stop / bias-flip / target) against the fill **held into the cycle** (SimBroker's "update state AFTER taking the outcome" discipline), never the same-cycle price the stop breach is tested against. Encoded as intra-node register state (survives the C23 flat-graph reorder; it is NOT a graph cycle, so it needs no back-edge support). A RED no-look-ahead test is mandatory from day one, parallel to `sim_broker_no_lookahead`: a bias-flip-exit trade's R must equal the lagged-fill R, and a stopped trade is exactly −1R independent of fill in the no-gap case. **The per-trade R-outcome is a sparse multi-column record (C8-safe).** position-management returns `Some(record)` only on a close cycle, `None` otherwise — a single net position closes ≤1 trade per cycle (a reversal closes one leg and reopens; the reopen is not an outcome), the genuine asymmetry vs. the position-event table (which needs >1/instant). Record columns: `r_multiple: f64` (authoritative, debug-asserted against `dir·(exit−entry)/|entry−stop|`), `exit_reason: i64` (enum: stop / bias-flip / reversal-leg / window-end), `was_stopped: bool` (the one attribution prices can't losslessly reconstruct under gaps), `entry_ts`/`exit_ts: timestamp`, `entry_price`/`stop_price`/`exit_price: f64`, `direction: i64 ∈ {−1,+1}`, `bias_at_entry_abs: f64` (conviction, for the calibration diagnostic), `size: f64`. **The R-evaluator is a POST-RUN FOLD (`summarize_r`), not an in-graph node.** Recorder taps are drained post-run (`rx.try_iter().collect()`; the playground is web-from-disk, C22 amendment) — nothing consumes a recorder stream mid-run, so an in-graph R-curve node buys nothing and double-maintains state with a drift risk and a broken (sparse, gapped) time axis. Stage 1: position-management emits the sparse R-outcomes → a `Recorder` taps them → `summarize_r` (sibling of `summarize`) folds the trade ledger into the metrics and a by-trade cumulative-R curve (running partial-sums, labelled by-trade, not time-drawdown). The in-graph running-equity node is held in reserve for Stage 2, where compounding genuinely needs it through the z⁻¹ fill-edge register. **The Sizer is a load-bearing seam (`size = risk_budget / stop_distance`, flat-1R).** "size = 1.0 constant" is constant-*notional*, not flat-1R; true flat-1R is risk-per-trade constant ⇒ size varies inversely with the stop. The Stage-1 body emits `size = 1 / stop_distance` (risk_budget = 1R), so it actually reads its `stop_distance` input and the interface is *exercised* — Stage 2 swaps the constant for `risk_fraction · equity`, same shape. R is computed **size-invariantly** by position-management, so size never contaminates the R-multiple (the property that makes Stage 1 feed-forward). Two tests: the flat-1R invariant (`size·stop_distance ≡ risk_budget`) and R-invariance under size scaling. **The Veto is OMITTED as a runtime node in Stage 1.** A pass-through identity node is exactly what C19/C23 DCE is licensed to delete; pinning it to survive would be an implicit-eval-order dependency C23 forbids. It is reserved as a *documented seam* (a named position in the spec, not a `BlueprintNode`), added as a real gating node with a real edge the cycle it first vetoes. **Conviction: sign-only sizing, but magnitude is measured.** flat-1R + `sign(bias)` entry, equal-weighted E[R] as the headline (do NOT conviction-weight the fold — that leaks sizing into the yardstick). But `|bias_at_entry|` is recorded per trade and `summarize_r` reports a **conviction-calibration diagnostic** (per-tercile E[R] / rank-corr(|bias|,R)), so a strategy whose edge is conviction-ranking (E[R]≈0 equal-weighted, strong corr(|bias|,R)) is not mis-reported as worthless. The deferred `|bias|>θ` threshold falls out as the 2-bucket read. **The RiskExecutor is a per-symbol Composite**, bias+price as input roles (price fanned to stop-rule and position-management — `GraphBuilder::feed` supports fan-out), exposing the R-outcome; `summarize_r` (the R-evaluator) sits downstream/outside, folding the recorded outcomes (matches C10 verbatim and the SimBroker-emits / summarize-folds pattern). The Broker/Account composite (nesting + equity aggregation) is Stage 2, not built now. **Metric set** (all new `RunMetrics` fields `#[serde(default)]` for `runs.jsonl` back-compat): E[R], win_rate, n_trades, avg_win_R, avg_loss_R, profit_factor, max by-trade R-drawdown, **SQN** (`√n · meanR / stdevR`, the dispersion-adjusted single-number sweep objective), the conviction diagnostic, and a **net-of-cost E[R]** (one round-trip spread, a price-unit param — still account-agnostic — so a churny strategy's R can't look fine while costs would sink it). Open trades at window end are **force-closed mark-to-close with `exit_reason = window-end`** and their count surfaced — never silently folded as unrealized MtM (the bug `summarize` has today). ### Note on substrate This is NEW discrete-trade machinery, not a SimBroker extension: SimBroker integrates a *continuous* exposure·return into a pip curve (no trade, no stop, no R) — it is the ancestor in spirit only. Stage 1 adds a discrete close-detector + stop + entry/exit pairing producing realised per-trade R. ### Iteration cut - **Commit 0 (head):** `exposure → bias` rename (#126) — behaviour-preserving, compiler-driven, its own commit. - **Iter 1:** `VolStop` + `FixedStop` (#119) + position-management lifecycle producing the size-invariant sparse R-outcome + the entry-latch / lagged-fill discipline + a core `summarize_r` fold (#127 core, #129 core). A complete, runnable, testable R-producing vertical slice (`source → bias → stop-rule → position-management → Recorder`, folded by `summarize_r`); R-invariance is the load-bearing property pinned here. - **Iter 2:** the load-bearing Sizer seam + the RiskExecutor composite (#128, Veto documented-not-built) + the enriched `summarize_r` (SQN, conviction diagnostic, net-of-cost, window-end handling) + the new `RunMetrics` fields + the CLI/recording surface (#129). Proceeding to `specify` with this as the ratified design.
Author
Owner

Refinement (spec-time, 0065): decision #5 (per-trade R-outcome) is sharpened from a sparse Option stream (Some-on-close / None) to a dense per-cycle record (one record/eval, always Some, like SimBroker) carrying a closed_this_cycle flag. Reason: a sparse stream cannot express the R-equity curve / max-drawdown-in-R (no intra-trade mark) and cannot reveal a position still OPEN at window end (juror D3, strongest point). The dense record subsumes both: the trade ledger is the closed-flagged rows; R-equity = cum_realized_r + unrealized_r; the window-end open trade is the last row (explicit, flagged, counted forced-close — no silent MtM). C8 still holds (one record/eval) and the ≤1-close/cycle asymmetry vs the position-event table is intact. Multi-field output is on a tested path (the Resample out:[Cell;4] precedent; #47 is only the CLI re-export rendering, orthogonal). Full design in spec 0065.

Refinement (spec-time, 0065): decision #5 (per-trade R-outcome) is sharpened from a **sparse Option stream** (Some-on-close / None) to a **dense per-cycle record** (one record/eval, always Some, like SimBroker) carrying a `closed_this_cycle` flag. Reason: a sparse stream cannot express the R-equity curve / max-drawdown-in-R (no intra-trade mark) and cannot reveal a position still OPEN at window end (juror D3, strongest point). The dense record subsumes both: the trade ledger is the closed-flagged rows; R-equity = cum_realized_r + unrealized_r; the window-end open trade is the last row (explicit, flagged, counted forced-close — no silent MtM). C8 still holds (one record/eval) and the ≤1-close/cycle asymmetry vs the position-event table is intact. Multi-field output is on a tested path (the Resample out:[Cell;4] precedent; #47 is only the CLI re-export rendering, orthogonal). Full design in spec 0065.
Author
Owner

Iteration-2 fork decisions (planner, derived — boss). Iter-1 committed 2c43296 (green).

  • summarize_r gains a round_trip_cost: f64 param (price units); the 6 callers pass 0.0 for gross. net_expectancy_r = mean over trades of (realized_r - round_trip_cost/|entry_price-stop_price|): a fixed price-spread costs MORE R when the stop is tighter (correct churn-honesty, H1). latched_dist is recovered per closed row from record cols 6/7; r_col gains ENTRY_PRICE=6, STOP_PRICE=7, BIAS_AT_ENTRY_ABS=9.
  • RunMetrics.r: Option carries BOTH #[serde(default)] AND #[serde(skip_serializing_if="Option::is_none")] — keeps the exact-byte to_json pins (report.rs, registry legacy-line) stable (None renders nothing) AND gives runs.jsonl back-compat (missing -> None).
  • conviction_terciles_r: closed trades (+ the window-end synthesized one) sorted by |bias_at_entry| (col 9), split into 3 contiguous equal-count terciles, equal-weighted E[R] each; <3 trades -> [0,0,0].
  • SQN = sqrt(n)*mean_R/stdev_R (sample stdev, n-1); n<2 or zero-variance -> 0.0.
  • RiskExecutor composite home (architecture fork): iter-2 builds it as a TEST FIXTURE in aura-engine (proves stop-rule->Sizer->position-management bootstraps+runs+matches the hand-wired chain). A SHIPPABLE reusable RiskExecutor cannot live cleanly yet: aura-std has no GraphBuilder, and aura-engine only dev-depends on aura-std. The shippable home is deferred to iter-3 (the CLI/aura-cli, which depends on aura-std+aura-engine normally and is the natural consumer) — no premature crate-layering change.
  • PositionManagement gains a 4th input slot "size" (the Sizer feeds it); R stays size-INVARIANT (a RED test asserts scaling risk_budget leaves every realized_r unchanged).
    Iter-2 scope = Sizer + RiskExecutor(fixture) + summarize_r enrichment + RunMetrics.r. CLI/recording surface = iter-3.
Iteration-2 fork decisions (planner, derived — boss). Iter-1 committed 2c43296 (green). - summarize_r gains a `round_trip_cost: f64` param (price units); the 6 callers pass 0.0 for gross. **net_expectancy_r** = mean over trades of (realized_r - round_trip_cost/|entry_price-stop_price|): a fixed price-spread costs MORE R when the stop is tighter (correct churn-honesty, H1). latched_dist is recovered per closed row from record cols 6/7; r_col gains ENTRY_PRICE=6, STOP_PRICE=7, BIAS_AT_ENTRY_ABS=9. - **RunMetrics.r: Option<RMetrics>** carries BOTH #[serde(default)] AND #[serde(skip_serializing_if="Option::is_none")] — keeps the exact-byte to_json pins (report.rs, registry legacy-line) stable (None renders nothing) AND gives runs.jsonl back-compat (missing -> None). - **conviction_terciles_r**: closed trades (+ the window-end synthesized one) sorted by |bias_at_entry| (col 9), split into 3 contiguous equal-count terciles, equal-weighted E[R] each; <3 trades -> [0,0,0]. - **SQN** = sqrt(n)*mean_R/stdev_R (sample stdev, n-1); n<2 or zero-variance -> 0.0. - **RiskExecutor composite home (architecture fork): iter-2 builds it as a TEST FIXTURE in aura-engine** (proves stop-rule->Sizer->position-management bootstraps+runs+matches the hand-wired chain). A SHIPPABLE reusable RiskExecutor cannot live cleanly yet: aura-std has no GraphBuilder, and aura-engine only dev-depends on aura-std. The shippable home is deferred to iter-3 (the CLI/aura-cli, which depends on aura-std+aura-engine normally and is the natural consumer) — no premature crate-layering change. - PositionManagement gains a 4th input slot "size" (the Sizer feeds it); R stays size-INVARIANT (a RED test asserts scaling risk_budget leaves every realized_r unchanged). Iter-2 scope = Sizer + RiskExecutor(fixture) + summarize_r enrichment + RunMetrics.r. CLI/recording surface = iter-3.
Author
Owner

Correction (2026-06-24): VolStop was a design error — a fused node justified by "no Abs/Mul primitive exists". Per the user, that reasoning is inverted: a node is a PRIMITIVE only if it is NOT DAG-expressible from other primitives; if a function needs a missing primitive, ADD the primitive. VolStop is pure feed-forward arithmetic -> a COMPOSITION, not a primitive.

Decisions:

  • Add two genuine primitives to aura-std: Mul (two-stream f64 product) and Sqrt (f64 -> sqrt). Mul was the missing one the user flagged ("we need a multiplication anyway").
  • The volatility stop becomes rolling stddev (EWMA volatility, RiskMetrics-style), NOT the earlier MAD/|Delta| hack: stop_distance = k * Sqrt(Ema(Mul(Delta,Delta), length)) where Delta = Sub(price, Delay(price,1)). This uses Mul (Delta^2) and Sqrt (variance price^2 -> stddev price, mandatory so stop_distance is in price units and R stays dimensionless). Abs is therefore NOT added (Delta^2 not |Delta|).
  • k*sigma is scaled via the existing LinComb (k a param weight), NOT Mul(Const(k),sigma): a source-less Const has no firing trigger in the push model.
  • FixedStop is KEPT as a legitimate primitive (a param constant gated on its price input — the correct shape for a triggered constant; a source-less Const would be broken). Not generalized to Const.
  • The fused VolStop node is REMOVED. The vol stop is now a vol_stop(length,k) -> Composite helper in aura-engine (composites need GraphBuilder, which aura-std lacks; same home as the RiskExecutor fixture).
  • PositionManagement, Latch, Delay, Sma/Ema, SimBroker stay legitimate primitives (state machines / windowed reductions with control flow, not pure-arithmetic DAGs). Only VolStop violated the principle.

This lands as a corrective mini-iteration BEFORE iter-2 (Sizer/RiskExecutor), since iter-2 builds on the stop-rule. iter-1 commit 2c43296 stop_rule.rs is revised; the iter-1 E2E used FixedStop, so it is unaffected.

Correction (2026-06-24): VolStop was a design error — a fused node justified by "no Abs/Mul primitive exists". Per the user, that reasoning is inverted: a node is a PRIMITIVE only if it is NOT DAG-expressible from other primitives; if a function needs a missing primitive, ADD the primitive. VolStop is pure feed-forward arithmetic -> a COMPOSITION, not a primitive. Decisions: - Add two genuine primitives to aura-std: Mul (two-stream f64 product) and Sqrt (f64 -> sqrt). Mul was the missing one the user flagged ("we need a multiplication anyway"). - The volatility stop becomes rolling stddev (EWMA volatility, RiskMetrics-style), NOT the earlier MAD/|Delta| hack: stop_distance = k * Sqrt(Ema(Mul(Delta,Delta), length)) where Delta = Sub(price, Delay(price,1)). This uses Mul (Delta^2) and Sqrt (variance price^2 -> stddev price, mandatory so stop_distance is in price units and R stays dimensionless). Abs is therefore NOT added (Delta^2 not |Delta|). - k*sigma is scaled via the existing LinComb (k a param weight), NOT Mul(Const(k),sigma): a source-less Const has no firing trigger in the push model. - FixedStop is KEPT as a legitimate primitive (a param constant gated on its price input — the correct shape for a triggered constant; a source-less Const would be broken). Not generalized to Const. - The fused VolStop node is REMOVED. The vol stop is now a vol_stop(length,k) -> Composite helper in aura-engine (composites need GraphBuilder, which aura-std lacks; same home as the RiskExecutor fixture). - PositionManagement, Latch, Delay, Sma/Ema, SimBroker stay legitimate primitives (state machines / windowed reductions with control flow, not pure-arithmetic DAGs). Only VolStop violated the principle. This lands as a corrective mini-iteration BEFORE iter-2 (Sizer/RiskExecutor), since iter-2 builds on the stop-rule. iter-1 commit 2c43296 stop_rule.rs is revised; the iter-1 E2E used FixedStop, so it is unaffected.
Author
Owner

Stage-1 cycle 0065 — iteration 2 landed (node + metric layer), iteration 3 (CLI surface) is a checkpoint

Committed (autonomous, unpushed, this /boss run; session anchor 0998f9a):

  • 6e21495 plan 0067 (iter-2: Sizer + RiskExecutor + metric enrichment)
  • b4e8433 feat: the implementation, full suite 500 passed / 0 failed, clippy clean.

What shipped (the substantive Stage-1 core):

  • PositionManagement gains a 4th size input (col 10, from the Sizer); R stays
    size-INVARIANT — pinned by a RED test at the node and again end-to-end.
  • Sizer (aura-std): size = risk_budget / stop_distance, flat-1R; the Stage-2
    fixed-fractional sizer slots in unchanged.
  • summarize_r enrichment: SQN, conviction terciles, net-of-cost (round_trip_cost
    param, charged in R via the producer-recovered latched_dist). Net-of-cost is
    tested through the real producer→consumer seam, not only hand-built rows.
  • RunMetrics.r: Option<RMetrics> with serde(default, skip_serializing_if)
    legacy runs.jsonl deserialise to None, pip-only on-disk shape byte-unchanged.
  • RiskExecutor composite (aura-engine integration-test fixture): FixedStop → Sizer → PositionManagement behind open bias+price roles, price fanned to
    both; bias produced in-graph from the single price source (a second bias source
    would k-way-merge into separate cycles and mark stale prices). Veto = documented
    seam, not a node.

Held quality finding (verified + fixed, not auto-signed past): the executor
fixture read the dense size column (10) under a "lockstep" comment, but the
cross-crate layout guard pinned only cols 1/6/7/9 — col 10 was unguarded. Brought
col 10 under the guard (r_col_indices_match_producer_field_layout), so the
size-invariance read is drift-protected like the others.

Decision — iteration 3 (the CLI/recording surface, #129) is a checkpoint, not
autonomous continuation.
The node + metric layer is design-settled by spec 0065;
the CLI surface is not. Today every CLI command folds pip metrics via
summarize and never summarize_r; wiring R in is design-open and high-context
(the CLI is ~2000 lines). Genuine forks it raises:

  1. RiskExecutor's shippable home — iter-2 deliberately left it a test fixture
    ("shippable home deferred to iter-3"); where it lives as a real API
    (aura-engine composite helper? aura-std?) is undecided.
  2. The R sample harness — the existing sample harness is SMA-cross → Bias →
    SimBroker (pip). An R run needs bias → stop-rule → RiskExecutor → dense record →
    summarize_r. New harness shape + which stop/strategy.
  3. How R-scoring surfaces — additive RunMetrics.r on every run (the Option
    was designed for this), or a dedicated mode/command? Product-shape, partly user
    preference.
  4. Recording/charting of the dense R-record (web-from-disk viewer).

This is the natural milestone-chunk boundary (substantive core done & green) and a
high-context next iteration whose session shape is the user's call — so it bounces
to the user rather than continuing autonomously.

## Stage-1 cycle 0065 — iteration 2 landed (node + metric layer), iteration 3 (CLI surface) is a checkpoint **Committed** (autonomous, unpushed, this `/boss` run; session anchor `0998f9a`): - `6e21495` plan 0067 (iter-2: Sizer + RiskExecutor + metric enrichment) - `b4e8433` feat: the implementation, full suite **500 passed / 0 failed**, clippy clean. **What shipped (the substantive Stage-1 core):** - `PositionManagement` gains a 4th `size` input (col 10, from the Sizer); R stays size-INVARIANT — pinned by a RED test at the node and again end-to-end. - `Sizer` (aura-std): `size = risk_budget / stop_distance`, flat-1R; the Stage-2 fixed-fractional sizer slots in unchanged. - `summarize_r` enrichment: SQN, conviction terciles, net-of-cost (`round_trip_cost` param, charged in R via the producer-recovered `latched_dist`). Net-of-cost is tested through the real producer→consumer seam, not only hand-built rows. - `RunMetrics.r: Option<RMetrics>` with `serde(default, skip_serializing_if)` — legacy `runs.jsonl` deserialise to `None`, pip-only on-disk shape byte-unchanged. - `RiskExecutor` composite (aura-engine integration-test fixture): `FixedStop → Sizer → PositionManagement` behind open `bias`+`price` roles, price fanned to both; bias produced in-graph from the single price source (a second bias *source* would k-way-merge into separate cycles and mark stale prices). Veto = documented seam, not a node. **Held quality finding (verified + fixed, not auto-signed past):** the executor fixture read the dense `size` column (10) under a "lockstep" comment, but the cross-crate layout guard pinned only cols 1/6/7/9 — col 10 was unguarded. Brought col 10 under the guard (`r_col_indices_match_producer_field_layout`), so the size-invariance read is drift-protected like the others. **Decision — iteration 3 (the CLI/recording surface, #129) is a checkpoint, not autonomous continuation.** The node + metric layer is design-settled by spec 0065; the **CLI surface is not.** Today every CLI command folds pip metrics via `summarize` and never `summarize_r`; wiring R in is design-open and high-context (the CLI is ~2000 lines). Genuine forks it raises: 1. **RiskExecutor's shippable home** — iter-2 deliberately left it a test fixture ("shippable home deferred to iter-3"); where it lives as a real API (aura-engine composite helper? aura-std?) is undecided. 2. **The R sample harness** — the existing sample harness is SMA-cross → Bias → SimBroker (pip). An R run needs bias → stop-rule → RiskExecutor → dense record → `summarize_r`. New harness shape + which stop/strategy. 3. **How R-scoring surfaces** — additive `RunMetrics.r` on every run (the Option was designed for this), or a dedicated mode/command? Product-shape, partly user preference. 4. **Recording/charting** of the dense R-record (web-from-disk viewer). This is the natural milestone-chunk boundary (substantive core done & green) and a high-context next iteration whose session shape is the user's call — so it bounces to the user rather than continuing autonomously.
Author
Owner

iteration 3 — the Stage-1 R CLI surface: fork decisions (boss-derived, user-delegated)

Context. User re-entered /boss (2026-06-24): "leg los. Du entscheidest das Design. Aura soll von der CLI aus bedienbar sein, vor allem vor dir selbst." Design authority for the iter-3 forks (enumerated in the comment above) is explicitly delegated to the orchestrator; the directive is the product-shape input that settles fork 3 — the CLI must be ergonomic to drive from a shell, especially by Claude itself.

Entry-path verdict. iter-3 continues the open cycle 0065 (boss-signed spec docs/specs/0065-stage1-r-signal-quality.md); it is not a new-cycle bounce. Every load-bearing fork is derivable (contracts C9/C17/C22 + the existing CLI code + the directive), so this is specify direct-entry — under /boss the autonomous signature is the grounding-check PASS.

Scope line drawn (#109 (a)/(b) fork, derived). "Operable from the CLI" = run + inspect a Rust-authored harness from the shell — the (a) reading that C9/C17/C22 already settle — not wiring/authoring topology in the CLI (the (b) reading, which would amend C17/C9 and needs its own brainstorm). The broad #109 meta-level (World, project-as-crate, composable orchestration) stays a separate future milestone; iter-3 is the narrow Stage-1-R single-run surface.

Fork decisions (one substantive sentence each):

  1. RiskExecutor's shippable home → an aura-engine composite-builder (pub fn risk_executor(...) -> Composite), promoted from the iter-2 test fixture: a composite needs GraphBuilder, which lives in aura-engine and not aura-std — the same placement already fixed for the vol_stop composite.

  2. R sample harness shape → SMA-cross → Bias → RiskExecutor(stop → Sizer → PositionManagement) → R-record tap, reusing the existing pip sample's signal generator so the R harness differs from the shipped pip sample by exactly the risk layer under test, with vol_stop the default stop because the milestone's value is a volatility-defined (not pip-rescaled) R.

  3. R-scoring surface → additive on aura run: the run path folds summarize_r into RunMetrics.r (the Option was designed for exactly this) and RunReport prints the R block when a dense R-record is present — one shell call, one readable result, the directive's "especially for Claude" ergonomics, with no separate mode to remember.

  4. Harness selection → aura run --harness <name> compile-time enumerated selector (sma | macd | stage1-r; the existing --macd kept as a back-compat alias): a fixed match over Rust-authored built-ins is pure running — C9/C17-clean, no runtime node registry, no DSL — and generalizes today's ad-hoc --macd flag into one uniform, extensible run verb.

  5. R-record charting → a minimal R-equity tap now, rich markers deferred: tapping the dense record's R-equity column (cum_realized_r + unrealized_r) reuses the existing trace-store + aura chart --tap rendering unchanged, so a usable R-curve costs one extra tap rather than a new viewer.

Deferred to follow-ons (to be filed as ideas):

  • R-sweep families + runs family rank sqn / rank expectancy_r (SQN as the documented sweep objective) — needs an R sweep-family builder, a larger surface than this single-run cut.
  • Rich R-record visualization (trade entry/exit markers, per-trade R bars, drawdown shading) — sibling of #118.
  • The broad #109 meta-level milestone.
## iteration 3 — the Stage-1 R CLI surface: fork decisions (boss-derived, user-delegated) **Context.** User re-entered `/boss` (2026-06-24): *"leg los. Du entscheidest das Design. Aura soll von der CLI aus bedienbar sein, vor allem vor dir selbst."* Design authority for the iter-3 forks (enumerated in the comment above) is explicitly delegated to the orchestrator; the directive is the product-shape input that settles fork 3 — the CLI must be ergonomic to drive from a shell, *especially by Claude itself*. **Entry-path verdict.** iter-3 continues the open cycle 0065 (boss-signed spec `docs/specs/0065-stage1-r-signal-quality.md`); it is **not** a new-cycle bounce. Every load-bearing fork is derivable (contracts C9/C17/C22 + the existing CLI code + the directive), so this is `specify` direct-entry — under `/boss` the autonomous signature is the grounding-check `PASS`. **Scope line drawn (#109 (a)/(b) fork, derived).** "Operable from the CLI" = *run + inspect* a Rust-authored harness from the shell — the **(a)** reading that C9/C17/C22 already settle — **not** wiring/authoring topology in the CLI (the **(b)** reading, which would amend C17/C9 and needs its own brainstorm). The broad #109 meta-level (World, project-as-crate, composable orchestration) stays a separate future milestone; iter-3 is the narrow Stage-1-R **single-run** surface. **Fork decisions (one substantive sentence each):** 1. **RiskExecutor's shippable home → an `aura-engine` composite-builder** (`pub fn risk_executor(...) -> Composite`), promoted from the iter-2 test fixture: a composite needs `GraphBuilder`, which lives in `aura-engine` and not `aura-std` — the same placement already fixed for the `vol_stop` composite. 2. **R sample harness shape → SMA-cross → Bias → RiskExecutor(stop → Sizer → PositionManagement) → R-record tap**, reusing the existing pip sample's signal generator so the R harness differs from the shipped pip sample by *exactly* the risk layer under test, with `vol_stop` the default stop because the milestone's value is a volatility-defined (not pip-rescaled) R. 3. **R-scoring surface → additive on `aura run`**: the run path folds `summarize_r` into `RunMetrics.r` (the `Option` was designed for exactly this) and `RunReport` prints the R block when a dense R-record is present — one shell call, one readable result, the directive's "especially for Claude" ergonomics, with no separate mode to remember. 4. **Harness selection → `aura run --harness <name>`** compile-time enumerated selector (`sma` | `macd` | `stage1-r`; the existing `--macd` kept as a back-compat alias): a fixed `match` over Rust-authored built-ins is pure *running* — C9/C17-clean, no runtime node registry, no DSL — and generalizes today's ad-hoc `--macd` flag into one uniform, extensible run verb. 5. **R-record charting → a minimal R-equity tap now, rich markers deferred**: tapping the dense record's R-equity column (`cum_realized_r + unrealized_r`) reuses the existing trace-store + `aura chart --tap` rendering unchanged, so a usable R-curve costs one extra tap rather than a new viewer. **Deferred to follow-ons (to be filed as ideas):** - R-sweep families + `runs family rank sqn` / `rank expectancy_r` (SQN as the documented sweep objective) — needs an R sweep-family builder, a larger surface than this single-run cut. - Rich R-record visualization (trade entry/exit markers, per-trade R bars, drawdown shading) — sibling of #118. - The broad #109 meta-level milestone.
Author
Owner

iter-3 spec signed (boss auto-sign) + the patch sub-decisions

Amended docs/specs/0065-stage1-r-signal-quality.md with the "Iteration 3 — the CLI surface" section and signed it under /boss (commit 08d09df, subject carries (boss-signed)). The autonomous signature is the grounding-check PASS — all substrate assumptions and the post-patch current-code claims ratified by named green tests.

An advisory 5-lens spec-skeptic panel ran alongside the gate: criterion / invariants / scope SOUND; ambiguity + plan-readiness returned BLOCK with real findings, resolved inline before signing (then re-grounded — the patch added new current-code claims, all ratified). The sub-decisions those findings forced, one sentence each:

  • argv → parse_run_args tokenizer: orthogonal --harness×--real×--trace composition is impossible under today's exhaustive literal-slice match, so the run arms become a tokenizer (same shape as the existing parse_real_args), --macd → alias, --harness sma the default.
  • round_trip_cost = 0.0: Stage-1 R is frictionless signal quality (costs are the Stage-2 broker concern), so the run path folds with zero cost and net == gross; a --cost knob is a follow-on.
  • persist_traces gains a third tap: the helper is 2-tap-hardwired, so the r_equity tap costs a signature/plumbing change (the chart viewer is genuinely unchanged; reduce_for_tap's MinMax default suits the curve).
  • dual-tap arg provenance pinned: pip summarize(equity, exposure) reads the SimBroker + Bias taps exactly as today; the RiskExecutor dense R-record is a third independent tap for summarize_r.
  • exposure_sign_flips stays: the iter-0 rename to bias_sign_flips was never carried out (the field is still exposure_sign_flips), so the example uses the as-built key; the rename is a deferred follow-on.

Reply vetoes; otherwise the plan proceeds.

## iter-3 spec signed (boss auto-sign) + the patch sub-decisions Amended `docs/specs/0065-stage1-r-signal-quality.md` with the "Iteration 3 — the CLI surface" section and signed it under `/boss` (commit `08d09df`, subject carries `(boss-signed)`). The autonomous signature is the **grounding-check PASS** — all substrate assumptions *and* the post-patch current-code claims ratified by named green tests. An advisory 5-lens spec-skeptic panel ran alongside the gate: criterion / invariants / scope **SOUND**; **ambiguity + plan-readiness returned BLOCK with real findings, resolved inline before signing** (then re-grounded — the patch added new current-code claims, all ratified). The sub-decisions those findings forced, one sentence each: - **argv → `parse_run_args` tokenizer:** orthogonal `--harness`×`--real`×`--trace` composition is impossible under today's exhaustive literal-slice match, so the `run` arms become a tokenizer (same shape as the existing `parse_real_args`), `--macd` → alias, `--harness sma` the default. - **`round_trip_cost = 0.0`:** Stage-1 R is frictionless signal quality (costs are the Stage-2 broker concern), so the run path folds with zero cost and `net == gross`; a `--cost` knob is a follow-on. - **`persist_traces` gains a third tap:** the helper is 2-tap-hardwired, so the `r_equity` tap costs a signature/plumbing change (the chart *viewer* is genuinely unchanged; `reduce_for_tap`'s MinMax default suits the curve). - **dual-tap arg provenance pinned:** pip `summarize(equity, exposure)` reads the SimBroker + Bias taps exactly as today; the RiskExecutor dense R-record is a third independent tap for `summarize_r`. - **`exposure_sign_flips` stays:** the iter-0 rename to `bias_sign_flips` was never carried out (the field is still `exposure_sign_flips`), so the example uses the as-built key; the rename is a deferred follow-on. Reply vetoes; otherwise the plan proceeds.
Author
Owner

Rename iteration (cycle 0065 tail, #126) — scope fork decided

User green-lit completing the exposure → bias rename tail. The load-bearing scope fork: which exposure occurrences are bias-residue to rename vs SimBroker's genuine exposure-integral concept to keep.

Decided (one sentence): rename only the bias-residue; keep SimBroker's pre-reframe exposure concept and the on-disk exposure tap label.

  • Rename: RunMetrics.exposure_sign_flipsbias_sign_flips with #[serde(alias = "exposure_sign_flips")] (the typed serde field the audit flagged as aging worst) + the parallel McAggregate/MetricStats field + the registry Metric::ExposureSignFlips rank vocab (accept BOTH rank strings for CLI back-compat, rename the variant, update the error/listing); the manifest param label exposure_scalebias_scale; the .named("exposure") Bias instances → .named("bias").
  • Keep: SimBroker's slot-0 exposure + prev_exposure + its exposure · price-return integral — the pre-reframe pip-quality node the 0065 spec retains unchanged (its input is conceptually an exposure it holds, fed by a bias); and the on-disk / user-facing exposure tap label (the recorded bias series feeding chart --tap exposure + the byte-pinned trace tests) — renaming it would break a stable chart selector + trace-file name for cosmetic gain.

Back-compat is the care-point: new output serializes bias_sign_flips / bias_scale; legacy runs.jsonl reads via the serde alias (the existing legacy-deserialize tests keep the old key, pinning the alias). RED-first. After this lands, #126 closes and the Stage-1 milestone container is finalized.

## Rename iteration (cycle 0065 tail, #126) — scope fork decided User green-lit completing the `exposure → bias` rename tail. The load-bearing scope fork: **which `exposure` occurrences are bias-residue to rename vs SimBroker's genuine exposure-integral concept to keep.** **Decided (one sentence): rename only the bias-residue; keep SimBroker's pre-reframe exposure concept and the on-disk `exposure` tap label.** - **Rename:** `RunMetrics.exposure_sign_flips` → `bias_sign_flips` with `#[serde(alias = "exposure_sign_flips")]` (the typed serde field the audit flagged as aging worst) + the parallel `McAggregate`/`MetricStats` field + the registry `Metric::ExposureSignFlips` rank vocab (accept BOTH rank strings for CLI back-compat, rename the variant, update the error/listing); the manifest param label `exposure_scale` → `bias_scale`; the `.named("exposure")` Bias instances → `.named("bias")`. - **Keep:** `SimBroker`'s slot-0 `exposure` + `prev_exposure` + its `exposure · price-return` integral — the **pre-reframe pip-quality node the 0065 spec retains unchanged** (its input is conceptually an exposure it holds, fed by a bias); and the on-disk / user-facing `exposure` **tap** label (the recorded bias series feeding `chart --tap exposure` + the byte-pinned trace tests) — renaming it would break a stable chart selector + trace-file name for cosmetic gain. Back-compat is the care-point: **new** output serializes `bias_sign_flips` / `bias_scale`; **legacy** `runs.jsonl` reads via the serde alias (the existing legacy-deserialize tests keep the old key, pinning the alias). RED-first. After this lands, #126 closes and the Stage-1 milestone container is finalized.
Author
Owner

Delivered: the C10 R-reframe (bias / R / RiskExecutor / Stage-1 vs Stage-2) is in the ledger (docs/design/INDEX.md C10, reframe note 2026-06-23) and the glossary carries every reframe term (bias, R-evaluator, RiskExecutor, Sizer, Stage 1/Stage 2, position-event table, realistic broker, R-metrics). This was the kickoff/decision-log issue for the reframe; its ledger+glossary deliverable is in the tree. Closing as done.

Delivered: the C10 R-reframe (bias / R / RiskExecutor / Stage-1 vs Stage-2) is in the ledger (docs/design/INDEX.md C10, reframe note 2026-06-23) and the glossary carries every reframe term (bias, R-evaluator, RiskExecutor, Sizer, Stage 1/Stage 2, position-event table, realistic broker, R-metrics). This was the kickoff/decision-log issue for the reframe; its ledger+glossary deliverable is in the tree. Closing as done.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#117