Cost-model graph (in R): composable cost nodes producing the net-R curve #148

Closed
opened 2026-06-28 11:58:27 +02:00 by Brummel · 14 comments
Owner

Per the 2026-06-28 C10 rework (ledger commit 29cdc8c, supersedes the retired realistic-broker milestone): cost in research is a composable C9 graph of cost nodes, in R, that approximates (never claims) realism.

Scope:

  • A cost-node abstraction in aura-std (+ a cost-graph composite-builder in aura-composites), never in the domain-free aura-engine.
  • Generalizes/subsumes the scalar round_trip_cost: summarize_r folds the cost-model's net-R rather than recomputing a scalar (one home for cost, no double-count).
  • Output: net R = gross R − cost-in-R; a net_r_equity tap/sink (sibling of r_equity). A bare gross-R run (no cost model) stays valid.
  • Cost nodes read the state they depend on (price, a realized-vol tap, a C11-recorded rate source, the executor's R-record / trade events) and emit cost-in-R. Per-trade factors (commission, flat cost) deduct at close; per-cycle-held factors (carry / funding) accrue over the hold. Reference geometry (pip value, stop-distance in currency) is read at ingestion to normalize a currency cost into R; notional size cancels (cost_in_R = cost_in_currency / (size·stop_dist)), so the model is R-pure without holding equity.
  • Discipline: each factor is a labelled stress-parameter OR data-grounded / falsifiable (e.g. realized-vol → slippage; recorded rates → swap). Default simple; complexity earned per grounded factor; over-modelling is the anti-pattern.
  • Conviction-based risk allocation as an R-aggregation axis (flat Σ realized_r vs conviction-weighted Σ |bias|·realized_r), distinct from the removed money-Sizer; tested via the existing conviction_at_entry / conviction_terciles_r.

Honesty: the net-R curve is a research / ranking hypothesis; the forward / live run is the ground truth (see the live/deploy-edge issue).

Likely a milestone container (cost-node abstraction → specific cost nodes → net_r_equity tap & charting → conviction axis); a brainstorm/specify scopes the first cycle.

Per the 2026-06-28 C10 rework (ledger commit `29cdc8c`, supersedes the retired realistic-broker milestone): cost in research is a **composable C9 graph of cost nodes, in R**, that approximates (never claims) realism. Scope: - A cost-node abstraction in `aura-std` (+ a cost-graph composite-builder in `aura-composites`), never in the domain-free `aura-engine`. - Generalizes/subsumes the scalar `round_trip_cost`: `summarize_r` folds the cost-model's net-R rather than recomputing a scalar (one home for cost, no double-count). - Output: **net R = gross R − cost-in-R**; a `net_r_equity` tap/sink (sibling of `r_equity`). A bare gross-R run (no cost model) stays valid. - Cost nodes read the state they depend on (price, a realized-vol tap, a C11-recorded rate source, the executor's R-record / trade events) and emit cost-in-R. Per-trade factors (commission, flat cost) deduct at close; per-cycle-held factors (carry / funding) accrue over the hold. Reference geometry (pip value, stop-distance in currency) is read at ingestion to normalize a currency cost into R; notional size cancels (`cost_in_R = cost_in_currency / (size·stop_dist)`), so the model is R-pure without holding equity. - Discipline: each factor is a labelled **stress-parameter** OR **data-grounded / falsifiable** (e.g. realized-vol → slippage; recorded rates → swap). Default simple; complexity earned per grounded factor; over-modelling is the anti-pattern. - Conviction-based risk allocation as an R-aggregation axis (flat `Σ realized_r` vs conviction-weighted `Σ |bias|·realized_r`), distinct from the removed money-Sizer; tested via the existing `conviction_at_entry` / `conviction_terciles_r`. Honesty: the net-R curve is a research / ranking hypothesis; the forward / live run is the ground truth (see the live/deploy-edge issue). Likely a milestone container (cost-node abstraction → specific cost nodes → `net_r_equity` tap & charting → conviction axis); a brainstorm/specify scopes the first cycle.
Brummel added the feature label 2026-06-28 11:58:27 +02:00
Brummel added this to the Cost-model graph (in R) milestone 2026-06-28 12:40:35 +02:00
Author
Owner

Cycle 1 scope — fork decisions (recorded under /boss, autonomous)

Routing: the design is settled in the sources (ledger C10 lines 480–679 + this issue), so this run enters through specify direct-entry, not an interactive brainstorm. Reference issue: this one. Session anchor: main a517899. Grounded by a 7-subsystem code recon (round_trip_cost, summarize_r/RMetrics, the r_equity tap, RiskExecutor/PM dense record, the aura-std Node substrate, the aura-composites builder idiom, ingestion geometry).

Decisions:

  • A — Cost is an in-graph node, not a post-run scalar. A cost node emits a cost_in_r stream; net_r_equity is its sink; summarize_r folds that same stream. Why: C10 mandates a composable C9 graph of cost nodes and names net_r_equity a sink (C8/C18 — only a sink can display), so the cost node is the single producer both consumers read (one home for cost, no double-count).

  • B — One concrete cost node + the integration seam this cycle; defer the general CostNode trait and the multi-node cost-graph composite-builder. Why: a one-implementor abstraction is speculative generality (the exact smell #147 was deferred for, 2026-06-27), and the issue's "abstraction + composite-builder" wording is the milestone breakdown, not binding cycle-1 scope ("scope the FIRST cycle minimally"). Checked with an adversarial scope-fork skeptic → SOUND: the seam is edge-wired, so an N-node cost graph is an additive upstream LinComb feeding the same one-edge seam unchanged — no corner.

  • C — First node: ConstantCost (a flat round-trip cost per trade, a labelled stress-parameter). Why: C10's "default simple" floor; it is the degenerate constant-per-trade case that subsumes the scalar round_trip_cost exactly (size = 1 ⇒ cost_in_r = cost_currency / latched_dist), keeping the C18 goldens byte-identical at cost = 0.

  • D — net_r_equity = r_equity − Σ cost_in_r, built with the same LinComb+Recorder idiom (a third summed term, weight −1, over a running cost sum). Why: C10 names it "sibling of the existing r_equity tap", so reuse the proven two-node tap construction.

  • E — R-pure normalization, no equity. cost_in_r = cost_currency / (size · latched_dist), with size flat-1R (unit) and latched_dist = |entry_price − stop_price| read from the dense record. Why: C10 — notional cancels, so the model is R-pure without ever holding equity.

  • F — A run with no cost node stays a valid gross-R baseline. Why: C10 — the cost layer is optional and additively composed-on; this is also what keeps the existing goldens byte-identical.

Deferred to later cycles (recorded, not built): data-grounded cost nodes (realized-vol → slippage, recorded-rate → swap), per-cycle-held accrual (carry / funding), the general CostNode trait + the cost-graph composite-builder (emerge when a second grounded factor exists), the conviction-weighting R-aggregation axis (flat Σ realized_r vs Σ |bias|·realized_r), percentage-of-notional cost (needs pip-value from InstrumentGeometry), the Veto seam, and the Sizer-port-drop cleanup (orthogonal).

Open implementation ambiguity handed to specify/planner (flagged by the skeptic, outside the scope-fork lens): how a per-cycle cost_in_r stream maps onto summarize_r's per-closed-trade fold — fire-only-on-close vs fire-every-cycle-then-filter — while preserving the byte-pinned net token form realized_r − cost_currency/latched_dist.

**Cycle 1 scope — fork decisions (recorded under `/boss`, autonomous)** Routing: the design is settled in the sources (ledger C10 lines 480–679 + this issue), so this run enters through `specify` direct-entry, not an interactive brainstorm. Reference issue: this one. Session anchor: main `a517899`. Grounded by a 7-subsystem code recon (round_trip_cost, summarize_r/RMetrics, the r_equity tap, RiskExecutor/PM dense record, the aura-std Node substrate, the aura-composites builder idiom, ingestion geometry). Decisions: - **A — Cost is an in-graph node, not a post-run scalar.** A cost node emits a `cost_in_r` stream; `net_r_equity` is its sink; `summarize_r` folds that same stream. Why: C10 mandates a composable C9 graph of cost nodes and names `net_r_equity` a sink (C8/C18 — only a sink can display), so the cost node is the single producer both consumers read (one home for cost, no double-count). - **B — One concrete cost node + the integration seam this cycle; defer the general `CostNode` trait and the multi-node cost-graph composite-builder.** Why: a one-implementor abstraction is speculative generality (the exact smell #147 was deferred for, 2026-06-27), and the issue's "abstraction + composite-builder" wording is the milestone breakdown, not binding cycle-1 scope ("scope the FIRST cycle minimally"). Checked with an adversarial scope-fork skeptic → SOUND: the seam is edge-wired, so an N-node cost graph is an additive upstream LinComb feeding the same one-edge seam unchanged — no corner. - **C — First node: `ConstantCost` (a flat round-trip cost per trade, a labelled stress-parameter).** Why: C10's "default simple" floor; it is the degenerate constant-per-trade case that subsumes the scalar `round_trip_cost` exactly (size = 1 ⇒ `cost_in_r = cost_currency / latched_dist`), keeping the C18 goldens byte-identical at cost = 0. - **D — `net_r_equity = r_equity − Σ cost_in_r`**, built with the same `LinComb`+`Recorder` idiom (a third summed term, weight −1, over a running cost sum). Why: C10 names it "sibling of the existing r_equity tap", so reuse the proven two-node tap construction. - **E — R-pure normalization, no equity.** `cost_in_r = cost_currency / (size · latched_dist)`, with size flat-1R (unit) and `latched_dist = |entry_price − stop_price|` read from the dense record. Why: C10 — notional cancels, so the model is R-pure without ever holding equity. - **F — A run with no cost node stays a valid gross-R baseline.** Why: C10 — the cost layer is optional and additively composed-on; this is also what keeps the existing goldens byte-identical. Deferred to later cycles (recorded, not built): data-grounded cost nodes (realized-vol → slippage, recorded-rate → swap), per-cycle-held accrual (carry / funding), the general `CostNode` trait + the cost-graph composite-builder (emerge when a second grounded factor exists), the conviction-weighting R-aggregation axis (flat Σ realized_r vs Σ |bias|·realized_r), percentage-of-notional cost (needs pip-value from InstrumentGeometry), the Veto seam, and the Sizer-port-drop cleanup (orthogonal). Open implementation ambiguity handed to `specify`/`planner` (flagged by the skeptic, outside the scope-fork lens): how a per-cycle `cost_in_r` stream maps onto `summarize_r`'s per-closed-trade fold — fire-only-on-close vs fire-every-cycle-then-filter — while preserving the byte-pinned net token form `realized_r − cost_currency/latched_dist`.
Author
Owner

Derived implementation decisions (specify Step 1.5) — the cost↔trade alignment

Grounding the seam against the live tree surfaced one load-bearing mechanic: the window-end trade is synthesized post-run by summarize_r (aura-analysis/src/lib.rs 174–184 — it scans record.last() and force-closes an open position from unrealized_r), it is not an in-graph close PM emits. A naive in-graph cost node (cost only on real closes) would therefore miss the window-end trade, misaligning the cost stream against summarize_r's ledger.

  • G — ConstantCost emits a 3-field record {cost_in_r, cum_cost_in_r, open_cost_in_r}, structurally isomorphic to PM's realized_r / cum_realized_r / unrealized_r triple. cost_in_r is charged on a real close (cost_per_trade/latched, else 0), cum_cost_in_r is its running sum, open_cost_in_r is the open position's would-be cost each cycle (else 0). Why: this is the one shape that feeds both consumers from a single producer (one home) AND lets summarize_r charge the synthesized window-end trade — so the node subsumes the scalar round_trip_cost exactly (charges every trade incl. window-end), which is a testable equivalence, not just a byte-identical cost=0 floor.

  • H — net_r_equity = LinComb(4, [1,1,−1,−1]) over [cum_realized_r, unrealized_r, cum_cost_in_r, open_cost_in_r] (r_equity's two-term sum plus the two cost terms). Why: its final point then equals summarize_r's net total algebraically — Σ_closed(r − cost) + (r_we − cost_we) — so the in-graph headline curve and the post-run scalar agree by construction.

  • I — summarize_r's scalar round_trip_cost: f64 is replaced by consuming the co-temporal cost stream (joined on ts; for each closed row read cost_in_r, for the window-end open last row read open_cost_in_r). A no-cost run passes an empty/absent cost stream ⇒ cost 0 ⇒ net token form realized_r − 0.0 ⇒ C18 goldens byte-identical. r_metrics_from_rs (the pooled-OOS copy) stays cost-free this cycle (cost in walk-forward pooling is deferred); the cross-reducer equality test still holds at cost=0.

Edge case noted for the plan (not load-bearing): on a reversal row (closed ∧ open) PM's entry/stop columns describe the CLOSED leg, so open_cost_in_r borrows the closed leg's latched for that one cycle (self-corrects next cycle); only observable if window-end coincides with a reversal.

**Derived implementation decisions (specify Step 1.5) — the cost↔trade alignment** Grounding the seam against the live tree surfaced one load-bearing mechanic: the **window-end trade is synthesized post-run by `summarize_r`** (aura-analysis/src/lib.rs 174–184 — it scans `record.last()` and force-closes an open position from `unrealized_r`), it is **not** an in-graph close PM emits. A naive in-graph cost node (cost only on real closes) would therefore miss the window-end trade, misaligning the cost stream against `summarize_r`'s ledger. - **G — `ConstantCost` emits a 3-field record `{cost_in_r, cum_cost_in_r, open_cost_in_r}`, structurally isomorphic to PM's `realized_r / cum_realized_r / unrealized_r` triple.** `cost_in_r` is charged on a real close (`cost_per_trade/latched`, else 0), `cum_cost_in_r` is its running sum, `open_cost_in_r` is the open position's would-be cost each cycle (else 0). Why: this is the one shape that feeds both consumers from a single producer (one home) AND lets `summarize_r` charge the synthesized window-end trade — so the node **subsumes the scalar `round_trip_cost` exactly** (charges every trade incl. window-end), which is a testable equivalence, not just a byte-identical cost=0 floor. - **H — `net_r_equity = LinComb(4, [1,1,−1,−1])` over `[cum_realized_r, unrealized_r, cum_cost_in_r, open_cost_in_r]`** (r_equity's two-term sum plus the two cost terms). Why: its final point then equals `summarize_r`'s net total algebraically — `Σ_closed(r − cost) + (r_we − cost_we)` — so the in-graph headline curve and the post-run scalar agree by construction. - **I — `summarize_r`'s scalar `round_trip_cost: f64` is replaced by consuming the co-temporal cost stream** (joined on ts; for each closed row read `cost_in_r`, for the window-end open last row read `open_cost_in_r`). A no-cost run passes an empty/absent cost stream ⇒ cost 0 ⇒ net token form `realized_r − 0.0` ⇒ C18 goldens byte-identical. `r_metrics_from_rs` (the pooled-OOS copy) stays cost-free this cycle (cost in walk-forward pooling is deferred); the cross-reducer equality test still holds at cost=0. Edge case noted for the plan (not load-bearing): on a reversal row (`closed ∧ open`) PM's `entry/stop` columns describe the CLOSED leg, so `open_cost_in_r` borrows the closed leg's latched for that one cycle (self-corrects next cycle); only observable if window-end coincides with a reversal.
Author
Owner

Plan-level derived decisions (planner, cycle 1 / spec 0081)

A file-structure recon surfaced two points the plan resolves (neither a design change; recorded for the audit trail):

  • J — Acceptance evidence is the JSON net_expectancy_r field + the chartable net_r_equity trace; no pretty-printer is added. The spec's worked console line (net E[R]: 0.31) is illustrative — aura run emits JSON ("net_expectancy_r": …), and there is no human-readable E[R] printer today. Adding one is out of cycle-1 scope; the cost-adjusted JSON field is the empirical evidence.

  • K — --cost-per-trade threads through the run AND sweep paths; walkforward / mc pass None. stage1_r_graph is shared by run / sweep / walkforward / mc. Run and sweep fold cost per-member via summarize_r (straightforward). Walkforward/mc pool out-of-sample R via r_metrics_from_rs, which stays cost-free this cycle (cost in OOS pooling is the deferred r_metrics_from_rs question), so those callers pass None. This honours the spec's "run / sweep / chart" scope while keeping the deferred boundary clean.

  • Blast-radius note (not a fork, recorded for accuracy): the summarize_r signature change (scalar → cost stream) touches 43 call expressions across 5 crates (9 production in aura-cli, 34 in tests), not the spec's "10". All migrate in the one signature-change task (the workspace will not compile until they do — a single compile unit); the 39 cost-0 sites become the empty-stream form, the 4 non-zero test sites (analysis + e2e) are rewritten to drive cost via the node / a constructed stream.

**Plan-level derived decisions (planner, cycle 1 / spec 0081)** A file-structure recon surfaced two points the plan resolves (neither a design change; recorded for the audit trail): - **J — Acceptance evidence is the JSON `net_expectancy_r` field + the chartable `net_r_equity` trace; no pretty-printer is added.** The spec's worked console line (`net E[R]: 0.31`) is illustrative — `aura run` emits JSON (`"net_expectancy_r": …`), and there is no human-readable E[R] printer today. Adding one is out of cycle-1 scope; the cost-adjusted JSON field is the empirical evidence. - **K — `--cost-per-trade` threads through the run AND sweep paths; walkforward / mc pass `None`.** `stage1_r_graph` is shared by run / sweep / walkforward / mc. Run and sweep fold cost per-member via `summarize_r` (straightforward). Walkforward/mc pool out-of-sample R via `r_metrics_from_rs`, which stays cost-free this cycle (cost in OOS pooling is the deferred `r_metrics_from_rs` question), so those callers pass `None`. This honours the spec's "run / sweep / chart" scope while keeping the deferred boundary clean. - **Blast-radius note (not a fork, recorded for accuracy):** the `summarize_r` signature change (scalar → cost stream) touches **43 call expressions across 5 crates** (9 production in aura-cli, 34 in tests), not the spec's "10". All migrate in the one signature-change task (the workspace will not compile until they do — a single compile unit); the 39 cost-0 sites become the empty-stream form, the 4 non-zero test sites (analysis + e2e) are rewritten to drive cost via the node / a constructed stream.
Author
Owner

Implement-time decisions ratified (cycle 1, spec 0081 — shipped, suite green)

The implement loop flagged two deviations from the plan's literal text for ratification; both are correct and accepted:

  • G' — the cost stream is recorded 3-wide (the node's full output), not 2-wide. The plan's Step 3 wired a 2-column cost Recorder [cost_in_r, open_cost_in_r], but Task 2's summarize_r reads open_cost_in_r at col 2 (the node's 3-field schema {cost_in_r=0, cum_cost_in_r=1, open_cost_in_r=2}). A 2-wide stream would index-out-of-bounds at col 2. Resolved 3-wide: the cost Recorder records the node's full output; summarize_r's cost_col reads col 0 (per-close) + col 2 (window-end open); col 1 (cum) feeds the in-graph net_r_equity. This is cleaner than cherry-picking columns (the lockstep contract is just "the node's schema"). The 2-wide block in the plan's Step 3 was the inconsistency, not the diff.

  • H' — Trade drops its latched field. With cost arriving already in R from the stream (net = r − cost), summarize_r no longer recovers latched_dist; the dead field would trip clippy -D warnings. r_col::ENTRY_PRICE/STOP_PRICE keep #[allow(dead_code)] (still the documented layout contract + exercised by the fixtures). Correctness-neutral, cleaner.

Also accepted (sensible implementer additions): a cost_per_trade >= 0 constructor guard (+ panic test) and a parameterised label "ConstantCost(2)" (the sibling convention, cf. SMA(2)).

Verified by the orchestrator: cargo build --workspace --all-targets clean, cargo test --workspace green (incl. the exact-subsumption test, the net-R E2E, and the byte-identical C18 no-cost golden), cargo clippy --workspace --all-targets -D warnings clean.

Pre-existing concern logged for the audit (not this cycle): aura-std/src/lib.rs module doc still names "realistic broker profiles", retired by the 2026-06-28 C10 rework.

**Implement-time decisions ratified (cycle 1, spec 0081 — shipped, suite green)** The implement loop flagged two deviations from the plan's literal text for ratification; both are correct and accepted: - **G' — the cost stream is recorded 3-wide (the node's full output), not 2-wide.** The plan's Step 3 wired a 2-column cost Recorder `[cost_in_r, open_cost_in_r]`, but Task 2's `summarize_r` reads `open_cost_in_r` at col 2 (the node's 3-field schema `{cost_in_r=0, cum_cost_in_r=1, open_cost_in_r=2}`). A 2-wide stream would index-out-of-bounds at col 2. Resolved 3-wide: the cost Recorder records the node's full output; `summarize_r`'s `cost_col` reads col 0 (per-close) + col 2 (window-end open); col 1 (cum) feeds the in-graph `net_r_equity`. This is cleaner than cherry-picking columns (the lockstep contract is just "the node's schema"). The 2-wide block in the plan's Step 3 was the inconsistency, not the diff. - **H' — `Trade` drops its `latched` field.** With cost arriving already in R from the stream (`net = r − cost`), `summarize_r` no longer recovers `latched_dist`; the dead field would trip `clippy -D warnings`. `r_col::ENTRY_PRICE`/`STOP_PRICE` keep `#[allow(dead_code)]` (still the documented layout contract + exercised by the fixtures). Correctness-neutral, cleaner. Also accepted (sensible implementer additions): a `cost_per_trade >= 0` constructor guard (+ panic test) and a parameterised label `"ConstantCost(2)"` (the sibling convention, cf. `SMA(2)`). Verified by the orchestrator: `cargo build --workspace --all-targets` clean, `cargo test --workspace` green (incl. the exact-subsumption test, the net-R E2E, and the byte-identical C18 no-cost golden), `cargo clippy --workspace --all-targets -D warnings` clean. Pre-existing concern logged for the audit (not this cycle): `aura-std/src/lib.rs` module doc still names "realistic broker profiles", retired by the 2026-06-28 C10 rework.
Author
Owner

Cycle 2 design record (specify, derived under /boss)

Second cost node for the "Cost-model graph (in R)" milestone. The design is
derived from the cycle-1 seam (the 3-field cost-in-R record, the net_r_equity
tap, the summarize_r fold) plus the C10 discipline; the genuinely-open
CostNode trait stays deferred. Each load-bearing fork below is a derived
orchestrator decision (not a user decision), recorded for after-the-fact audit.
Session anchor: main e82725f.

  • Fork: which 2nd cost nodeVolSlippageCost (slippage proportional to
    recent realized volatility).
    Basis: derived — the canonical data-grounded example named in the C10 rework;
    structurally different from ConstantCost (state-dependent, reads a vol
    tap), so it genuinely stress-tests whether the cost seam composes rather than
    cloning a constant.

  • Fork: honesty framing of the coefficient → a labelled stress-parameter
    slip_vol_mult (k); the volatility input itself is data-grounded (measured
    from price).
    Basis: derived from C10 — "real friction is not historically knowable; each
    factor is a labelled stress-parameter OR data-grounded/falsifiable." Slippage
    as a fraction of recent vol is a stress assumption, honestly labelled; the vol
    it scales is measured.

  • Fork: vol input source → the node reads a volatility f64 input stream
    (price units) from upstream, not computed internally; the E2E wires a simple
    rolling-range proxy (rolling_max − rolling_min of close) from existing nodes.
    Basis: derived from C9 composability — the cost node stays a pure cost-in-R
    transducer reading {trade signals} + {a vol tap}, mirroring how ConstantCost
    reads {closed, open, entry, stop}; the vol estimator stays swappable and no new
    indicator primitive is needed this cycle.

  • Fork: R-pure cost formcost_in_r = k·vol / |entry − stop|, charged on
    close (and open would-be on the window-end open row), emitted as the same
    3-field record {cost_in_r, cum_cost_in_r, open_cost_in_r} as ConstantCost.
    Basis: derived from C10's currency→R normalization — notional/size/currency
    cancel exactly as in ConstantCost; reusing the record shape keeps the cycle-1
    seam (summarize_r fold + net_r_equity tap) unchanged.

  • Fork: composition mechanism → when >1 cost node is present, their cost-in-R
    streams are summed per-field into one aggregate cost stream (via existing
    combinators); summarize_r and net_r_equity read the aggregate, unchanged.
    Basis: derived from C10 "composable C9 graph of cost nodes" + cycle-1's "one
    home for cost" — the aggregate is the cost-graph output; adding an Nth cost
    node later just adds an input to the aggregator, and summarize_r's
    single-stream positional-join contract is preserved (it now reads the
    aggregate). This is the cycle's real architectural deliverable: the proof that
    the cost graph composes.

  • Fork: general CostNode traitdeferred to a later cycle (cycle 3).
    Basis: derived — even with two nodes the trait is best designed against two
    shipped, working nodes rather than co-designed with the second; the seam
    already consumes the 3-field cost-in-R shape structurally, so the nodes compose
    without a trait, and formalizing it later is a clean behaviour-preserving
    refactor. One more cycle of "wait for concrete nodes" buys a better abstraction
    boundary.

  • Fork: CLI / scope → add --slip-vol-mult <f64> (analogous to
    --cost-per-trade), run-path-scoped; combinable with --cost-per-trade so
    composition is demonstrable end-to-end. Sweep/walkforward/mc still pass None.
    Basis: derived — mirrors cycle-1's run-path scoping; the reduce-mode sweep-path
    cost remains the named deferral, untouched here.

Acceptance: the cycle ships green when (a) the vol-slippage net == hand-computed
mean(rᵢ − k·volᵢ/|entry−stop|ᵢ) over all trades incl. the window-end one; (b)
two-node composition net == the exact per-trade sum of both costs; (c) the
no-cost baseline stays byte-identical (C18 golden held); (d) a single
vol-slippage run still folds correctly through the aggregate seam.

## Cycle 2 design record (specify, derived under /boss) Second cost node for the "Cost-model graph (in R)" milestone. The design is derived from the cycle-1 seam (the 3-field cost-in-R record, the `net_r_equity` tap, the `summarize_r` fold) plus the C10 discipline; the genuinely-open `CostNode` trait stays deferred. Each load-bearing fork below is a *derived* orchestrator decision (not a user decision), recorded for after-the-fact audit. Session anchor: main `e82725f`. - **Fork: which 2nd cost node** → `VolSlippageCost` (slippage proportional to recent realized volatility). Basis: derived — the canonical data-grounded example named in the C10 rework; structurally *different* from `ConstantCost` (state-dependent, reads a vol tap), so it genuinely stress-tests whether the cost seam composes rather than cloning a constant. - **Fork: honesty framing of the coefficient** → a **labelled stress-parameter** `slip_vol_mult` (k); the volatility input itself is data-grounded (measured from price). Basis: derived from C10 — "real friction is not historically knowable; each factor is a labelled stress-parameter OR data-grounded/falsifiable." Slippage as a fraction of recent vol is a stress assumption, honestly labelled; the vol it scales is measured. - **Fork: vol input source** → the node reads a `volatility` f64 **input stream** (price units) from upstream, not computed internally; the E2E wires a simple rolling-range proxy (`rolling_max − rolling_min` of close) from existing nodes. Basis: derived from C9 composability — the cost node stays a pure cost-in-R transducer reading {trade signals} + {a vol tap}, mirroring how `ConstantCost` reads {closed, open, entry, stop}; the vol estimator stays swappable and no new indicator primitive is needed this cycle. - **Fork: R-pure cost form** → `cost_in_r = k·vol / |entry − stop|`, charged on close (and open would-be on the window-end open row), emitted as the same 3-field record {cost_in_r, cum_cost_in_r, open_cost_in_r} as `ConstantCost`. Basis: derived from C10's currency→R normalization — notional/size/currency cancel exactly as in `ConstantCost`; reusing the record shape keeps the cycle-1 seam (`summarize_r` fold + `net_r_equity` tap) unchanged. - **Fork: composition mechanism** → when >1 cost node is present, their cost-in-R streams are summed **per-field** into one aggregate cost stream (via existing combinators); `summarize_r` and `net_r_equity` read the aggregate, unchanged. Basis: derived from C10 "composable C9 graph of cost nodes" + cycle-1's "one home for cost" — the aggregate is the cost-graph output; adding an Nth cost node later just adds an input to the aggregator, and `summarize_r`'s single-stream positional-join contract is preserved (it now reads the aggregate). This is the cycle's real architectural deliverable: the proof that the cost graph composes. - **Fork: general `CostNode` trait** → **deferred** to a later cycle (cycle 3). Basis: derived — even with two nodes the trait is best designed against two *shipped, working* nodes rather than co-designed with the second; the seam already consumes the 3-field cost-in-R shape structurally, so the nodes compose without a trait, and formalizing it later is a clean behaviour-preserving refactor. One more cycle of "wait for concrete nodes" buys a better abstraction boundary. - **Fork: CLI / scope** → add `--slip-vol-mult <f64>` (analogous to `--cost-per-trade`), run-path-scoped; combinable with `--cost-per-trade` so composition is demonstrable end-to-end. Sweep/walkforward/mc still pass None. Basis: derived — mirrors cycle-1's run-path scoping; the reduce-mode sweep-path cost remains the named deferral, untouched here. Acceptance: the cycle ships green when (a) the vol-slippage net == hand-computed mean(rᵢ − k·volᵢ/|entry−stop|ᵢ) over all trades incl. the window-end one; (b) two-node composition net == the exact per-trade sum of both costs; (c) the no-cost baseline stays byte-identical (C18 golden held); (d) a single vol-slippage run still folds correctly through the aggregate seam.
Author
Owner

Cycle 2 design correction (discovered in implementation) — co-temporality contract

The implement-loop correctly blocked Task 3 on a real design flaw in the
signed spec (the gate did its job — no green-but-wrong shipped). Recording the
derived re-decision that resolved it.

  • Flaw: the spec had VolSlippageCost withhold (mode-A) until all five
    inputs are present, including volatility. The vol input is a
    RollingMax−RollingMin proxy that warms up later than the PM record, so the
    cost stream became delayed/short and no longer co-temporal 1:1 with the PM
    record
    . summarize_r positional-joins cost[i] ↔ record[i], so the desync
    made it read ~0 cost → net_both == gross. (Compounding: SLIP_VOL_LENGTH=20 >
    the 18-tick synthetic fixture, so the proxy never warmed at all.)

  • Re-decision (derived): the co-temporality contract. A cost node's stream
    MUST stay 1:1 with the PM record, so a cost node is gated only by the PM
    trade-geometry
    (closed/open/entry/stop); any not-yet-warm state input
    (volatility here, a recorded rate later) contributes 0 cost that cycle —
    the node still emits its row. This makes co-temporality structural and
    warmup-independent, preserves the C18 golden, and generalizes to any future
    state-dependent cost factor. ConstantCost satisfies it trivially; only
    VolSlippageCost needed the missing-factor→0 rule.
    Basis: derived — the positional-join co-temporality is the load-bearing seam
    contract from cycle 1; charging 0 during a factor's warm-up (no estimate yet →
    no charge) is the honest, contract-preserving choice. The alternative (a
    key-join in summarize_r) was rejected: it would move the C18 golden and push
    cost-graph logic into the post-run fold (against C10's "cost is a graph").

  • SLIP_VOL_LENGTH 20 → 5. A short realized-range window, still distinct from
    the stop's EWMA-3, but short enough to warm within the 18-tick synthetic smoke
    fixture so the run path exercises non-zero slippage (on real M1 data any
    reasonable window warms).

Verified: full workspace suite green (0 fail), clippy -D warnings clean, the
C18 no-cost golden byte-identical, the CLI composition run shows net_both < net_flat (vol-slippage bites on top of the flat cost), and the node-level
exact-additive composition + aggregate-net-agreement + CostSum(1)-identity
tests pass. Spec 0082 + the plan were amended to the corrected contract.

## Cycle 2 design correction (discovered in implementation) — co-temporality contract The implement-loop **correctly blocked** Task 3 on a real design flaw in the signed spec (the gate did its job — no green-but-wrong shipped). Recording the derived re-decision that resolved it. - **Flaw:** the spec had `VolSlippageCost` withhold (mode-A) until **all five** inputs are present, including `volatility`. The vol input is a `RollingMax−RollingMin` proxy that warms up later than the PM record, so the cost stream became **delayed/short and no longer co-temporal 1:1 with the PM record**. `summarize_r` positional-joins `cost[i] ↔ record[i]`, so the desync made it read ~0 cost → `net_both == gross`. (Compounding: `SLIP_VOL_LENGTH=20 >` the 18-tick synthetic fixture, so the proxy never warmed at all.) - **Re-decision (derived): the co-temporality contract.** A cost node's stream MUST stay 1:1 with the PM record, so a cost node is gated **only by the PM trade-geometry** (`closed`/`open`/`entry`/`stop`); any not-yet-warm state input (volatility here, a recorded rate later) contributes **0 cost** that cycle — the node still emits its row. This makes co-temporality structural and warmup-independent, preserves the C18 golden, and generalizes to any future state-dependent cost factor. `ConstantCost` satisfies it trivially; only `VolSlippageCost` needed the missing-factor→0 rule. Basis: derived — the positional-join co-temporality is the load-bearing seam contract from cycle 1; charging 0 during a factor's warm-up (no estimate yet → no charge) is the honest, contract-preserving choice. The alternative (a key-join in `summarize_r`) was rejected: it would move the C18 golden and push cost-graph logic into the post-run fold (against C10's "cost is a graph"). - **`SLIP_VOL_LENGTH` 20 → 5.** A short realized-range window, still distinct from the stop's EWMA-3, but short enough to warm within the 18-tick synthetic smoke fixture so the run path exercises non-zero slippage (on real M1 data any reasonable window warms). Verified: full workspace suite green (0 fail), clippy `-D warnings` clean, the C18 no-cost golden byte-identical, the CLI composition run shows `net_both < net_flat` (vol-slippage bites on top of the flat cost), and the node-level exact-additive composition + aggregate-net-agreement + `CostSum(1)`-identity tests pass. Spec 0082 + the plan were amended to the corrected contract.
Author
Owner

Cycle 3 scope — fork decisions (recorded under /boss, autonomous)

User directive: /boss CostNode-Trait, leg los — explicit green-light for the
twice-deferred CostNode trait cycle (the trait + cost-graph composite-builder
named in this issue's scope). Session anchor: main 6c7f5dd (cycle-2 close).

Routing (entry-path cascade): spec-driven (specify direct-entry), not brainstorm.
The forks are derivable — the trait's shape is dictated by the diff between the
two already-shipped cost nodes (ConstantCost, VolSlippageCost) plus CostSum,
and the position_management::FIELD_NAMES/WIDTH precedent. No fork hangs on pure
user preference, so this is settled-in-sources work, decided here. Grounded by a
node-authoring-surface recon (Node/Ctx/Cell/PrimitiveBuilder in aura-core, the 3
cost nodes, the main.rs cost-block wiring, the aura-composites builder idiom).

Decisions (all derived, with rationale):

  • A — A factor trait CostNode + a generic runner CostRunner<F: CostNode>, NOT
    a blanket impl<T: CostNode> Node for T.
    Why: the co-temporality skeleton needs
    shared mutable state (cum: f64, out: [Cell;3]); a trait cannot add fields, the
    runner can. The recon found zero blanket impls in crates/ (the norm is concrete
    types impl Node), so a blanket impl is architecturally novel; the generic wrapper
    impl<F> Node for CostRunner<F> on a concrete type is idiomatic and keeps the
    numerator call inlinable (no vtable on the hot path).
  • B — The trait's single hook is the price-unit cost numerator,
    fn cost_numerator(&mut self, ctx: &Ctx<'_>) -> f64, plus
    fn extra_inputs() -> Vec<PortSpec> (default none); the runner passes the full Ctx.

    Why: the numerator is literally the only behavioural difference between the two
    nodes (cost_per_trade vs slip_vol_mult * vol). Passing the full Ctx (it is
    Copy) future-proofs a notional-% cost that must read entry_price from the
    geometry slots, at no cost over a restricted view. The runner owns geometry gating,
    latched, the closed/open charge, cum, and the 3-field emit — once.
  • C — One source of truth for the 3-field cost triple
    (COST_FIELD_NAMES/COST_WIDTH in a new aura-std/src/cost.rs), read by BOTH the
    runner's schema helper AND CostSum.
    Why: mirrors the
    position_management::FIELD_NAMES/WIDTH precedent and turns the cycle-2
    audit-flagged by-convention 3-field lockstep into a structural single-source
    contract — the cycle's core motivation, the thing that makes the trait non-speculative.
  • D — Migrate BOTH ConstantCost and VolSlippageCost onto the trait (thin
    factor impls; new() returns CostRunner<Self>). Why: the trait is justified
    precisely because ≥2 implementors now exist (this is the exact condition cycle-1
    deferred it for); migrating both removes the duplicated skeleton and makes the
    lockstep structural. Byte-identity is preserved — the runner computes
    numerator / latched token-identical to the originals (cost_per_trade / latched,
    slip_vol_mult * vol / latched, left-associative-identical) — so every existing
    aura-std unit test + the aura-engine composition E2E + the C18 golden stay green
    verbatim. That is the migration's built-in regression net.
  • E — Defer the cost-graph composite-builder to a follow-up cycle. Why: it is
    wiring sugar over an already-working edge-wired seam (the cycle-1-recorded "additive
    upstream feeding the same one-edge seam unchanged"); the user's explicit ask was
    "CostNode-Trait"; bundling dilutes the cycle's single responsibility (abstraction +
    lockstep elimination). Honors "scope the FIRST cycle minimally" and the milestone
    breakdown order (cost-node abstraction first).
  • F — Acceptance constraint: behaviour-preserving. The builders produce unchanged
    schemas (same input/output port names), so the main.rs wiring, the net_r_equity
    seam, and summarize_r are untouched; the new author-facing surface is "write a
    cost node = impl CostNode with one numerator method."

Deferred (recorded, not built): the cost-graph composite-builder (decision E), the
data-grounded nodes (recorded-rate → swap), per-cycle-held accrual (carry/funding),
the conviction-weighting R-aggregation axis, sweep-path/OOS-pooling cost.

**Cycle 3 scope — fork decisions (recorded under `/boss`, autonomous)** User directive: `/boss CostNode-Trait, leg los` — explicit green-light for the twice-deferred `CostNode` trait cycle (the trait + cost-graph composite-builder named in this issue's scope). Session anchor: main `6c7f5dd` (cycle-2 close). **Routing (entry-path cascade): spec-driven (specify direct-entry), not brainstorm.** The forks are *derivable* — the trait's shape is dictated by the diff between the two already-shipped cost nodes (`ConstantCost`, `VolSlippageCost`) plus `CostSum`, and the `position_management::FIELD_NAMES/WIDTH` precedent. No fork hangs on pure user preference, so this is settled-in-sources work, decided here. Grounded by a node-authoring-surface recon (Node/Ctx/Cell/PrimitiveBuilder in aura-core, the 3 cost nodes, the main.rs cost-block wiring, the aura-composites builder idiom). Decisions (all *derived*, with rationale): - **A — A factor trait `CostNode` + a generic runner `CostRunner<F: CostNode>`, NOT a blanket `impl<T: CostNode> Node for T`.** Why: the co-temporality skeleton needs shared mutable state (`cum: f64`, `out: [Cell;3]`); a trait cannot add fields, the runner can. The recon found *zero* blanket impls in `crates/` (the norm is concrete types impl `Node`), so a blanket impl is architecturally novel; the generic wrapper `impl<F> Node for CostRunner<F>` on a concrete type is idiomatic and keeps the numerator call inlinable (no vtable on the hot path). - **B — The trait's single hook is the price-unit cost numerator, `fn cost_numerator(&mut self, ctx: &Ctx<'_>) -> f64`, plus `fn extra_inputs() -> Vec<PortSpec>` (default none); the runner passes the full Ctx.** Why: the numerator is literally the only behavioural difference between the two nodes (`cost_per_trade` vs `slip_vol_mult * vol`). Passing the full Ctx (it is `Copy`) future-proofs a notional-% cost that must read `entry_price` from the geometry slots, at no cost over a restricted view. The runner owns geometry gating, `latched`, the closed/open charge, `cum`, and the 3-field emit — once. - **C — One source of truth for the 3-field cost triple (`COST_FIELD_NAMES`/`COST_WIDTH` in a new `aura-std/src/cost.rs`), read by BOTH the runner's schema helper AND `CostSum`.** Why: mirrors the `position_management::FIELD_NAMES`/`WIDTH` precedent and turns the cycle-2 audit-flagged by-convention 3-field lockstep into a structural single-source contract — the cycle's core motivation, the thing that makes the trait non-speculative. - **D — Migrate BOTH `ConstantCost` and `VolSlippageCost` onto the trait** (thin factor impls; `new()` returns `CostRunner<Self>`). Why: the trait is justified precisely because ≥2 implementors now exist (this is the exact condition cycle-1 deferred it for); migrating both removes the duplicated skeleton and makes the lockstep structural. Byte-identity is preserved — the runner computes `numerator / latched` token-identical to the originals (`cost_per_trade / latched`, `slip_vol_mult * vol / latched`, left-associative-identical) — so every existing aura-std unit test + the aura-engine composition E2E + the C18 golden stay green verbatim. That is the migration's built-in regression net. - **E — Defer the cost-graph composite-builder to a follow-up cycle.** Why: it is wiring sugar over an already-working edge-wired seam (the cycle-1-recorded "additive upstream feeding the same one-edge seam unchanged"); the user's explicit ask was "CostNode-Trait"; bundling dilutes the cycle's single responsibility (abstraction + lockstep elimination). Honors "scope the FIRST cycle minimally" and the milestone breakdown order (cost-node abstraction first). - **F — Acceptance constraint: behaviour-preserving.** The builders produce unchanged schemas (same input/output port names), so the main.rs wiring, the `net_r_equity` seam, and `summarize_r` are untouched; the new author-facing surface is "write a cost node = `impl CostNode` with one numerator method." Deferred (recorded, not built): the cost-graph composite-builder (decision E), the data-grounded nodes (recorded-rate → swap), per-cycle-held accrual (carry/funding), the conviction-weighting R-aggregation axis, sweep-path/OOS-pooling cost.
Author
Owner

Cycle 3 (0083) — CostNode trait + shared cost-record contract — CLOSED, drift-clean

Commits (above anchor 6c7f5dd, unpushed): 16f4cbb spec (boss-signed) → c787488
plan → 6b53c23 feat → fc52b4f audit.

The twice-deferred general CostNode trait shipped. New aura-std/src/cost.rs owns
the cost-model node abstraction:

  • CostNode factor trait — a cost node's only per-node difference: the
    price-unit cost_numerator(&mut self, &Ctx) -> f64 (plus extra_inputs default-none
    • label).
  • CostRunner<F: CostNode> adapter — holds the shared cum/out state, impls
    Node, and writes the co-temporality skeleton (geometry-only gating,
    numerator / latched R-normalization, closed/open charge, cum, 3-field emit) ONCE.
  • One source of truth for the 3-field triple (COST_FIELD_NAMES / COST_WIDTH,
    mirroring position_management::{FIELD_NAMES, WIDTH}), read by both producers +
    CostSum + the CLI wiring. The cycle-0082 by-convention triple lockstep is
    structurally eliminated (four restatements collapsed to one).

ConstantCost and VolSlippageCost migrated to thin factors (new() returns
CostRunner<Self>). Behaviour-preserving — same schemas/wiring, byte-identical
numerator/latched token form; the existing suite passes verbatim and two new CLI
characterization goldens pin the exact flat/composed net_expectancy_r (the prior
tests only asserted net < gross, which a numerator drift would pass silently).

Plan deviation (sound): CostNode::name() was dropped as dead surface (nothing
consumes it; label() carries the name, cost_node_builder takes it explicitly) —
honours C23. Honours C9 (a CostRunner<F> is a plain downstream Node).

Verified: cargo build/test --workspace clean (0 failures), clippy -D warnings
clean, cargo doc -p aura-std clean. Ledger C10 carries a cycle-0083 realization note.

Still deferred (each its own cycle, recorded — not built):

  • the multi-node cost-graph composite-builder (decision E — wiring sugar over the
    already-working edge-wired seam; the natural next CostNode-area cycle);
  • data-grounded nodes (recorded-rate → swap);
  • per-cycle-held accrual (carry / funding — will force a summarize_r fold change);
  • the conviction-weighting R-aggregation axis (tdd-runnable);
  • sweep-path / OOS-pooling cost.

Minor carried debt: vol_slippage_cost.rs declares its extra-input port twice (the
cost_node_builder arg + the trait extra_inputs()); a clean unifier needs design
thought (the builder is a static method, no factor instance) — left for a follow-up.

Milestone "Cost-model graph (in R)" remains OPEN.

**Cycle 3 (0083) — CostNode trait + shared cost-record contract — CLOSED, drift-clean** Commits (above anchor `6c7f5dd`, unpushed): `16f4cbb` spec (boss-signed) → `c787488` plan → `6b53c23` feat → `fc52b4f` audit. The twice-deferred general `CostNode` trait shipped. New `aura-std/src/cost.rs` owns the cost-model node abstraction: - **`CostNode` factor trait** — a cost node's only per-node difference: the price-unit `cost_numerator(&mut self, &Ctx) -> f64` (plus `extra_inputs` default-none + `label`). - **`CostRunner<F: CostNode>` adapter** — holds the shared `cum`/`out` state, impls `Node`, and writes the co-temporality skeleton (geometry-only gating, `numerator / latched` R-normalization, closed/open charge, cum, 3-field emit) ONCE. - **One source of truth for the 3-field triple** (`COST_FIELD_NAMES` / `COST_WIDTH`, mirroring `position_management::{FIELD_NAMES, WIDTH}`), read by both producers + `CostSum` + the CLI wiring. The cycle-0082 by-convention triple lockstep is **structurally eliminated** (four restatements collapsed to one). `ConstantCost` and `VolSlippageCost` migrated to thin factors (`new()` returns `CostRunner<Self>`). Behaviour-preserving — same schemas/wiring, byte-identical `numerator/latched` token form; the existing suite passes verbatim and two new CLI characterization goldens pin the exact flat/composed `net_expectancy_r` (the prior tests only asserted `net < gross`, which a numerator drift would pass silently). Plan deviation (sound): `CostNode::name()` was dropped as dead surface (nothing consumes it; `label()` carries the name, `cost_node_builder` takes it explicitly) — honours C23. Honours C9 (a `CostRunner<F>` is a plain downstream `Node`). Verified: `cargo build/test --workspace` clean (0 failures), `clippy -D warnings` clean, `cargo doc -p aura-std` clean. Ledger C10 carries a cycle-0083 realization note. Still deferred (each its own cycle, recorded — not built): - **the multi-node cost-graph composite-builder** (decision E — wiring sugar over the already-working edge-wired seam; the natural next CostNode-area cycle); - data-grounded nodes (recorded-rate → swap); - per-cycle-held accrual (carry / funding — will force a `summarize_r` fold change); - the conviction-weighting R-aggregation axis (tdd-runnable); - sweep-path / OOS-pooling cost. Minor carried debt: `vol_slippage_cost.rs` declares its extra-input port twice (the `cost_node_builder` arg + the trait `extra_inputs()`); a clean unifier needs design thought (the builder is a static method, no factor instance) — left for a follow-up. Milestone "Cost-model graph (in R)" remains OPEN.
Author
Owner

Cycle 4 (cost-graph composite-builder) — kickoff (boss, autonomous)

/boss mach weiter after the cycle-3 push. Per the milestone-stop-policy
(the milestone is the stop-marker — run cycles toward it without a
per-cycle bounce-back) and this milestone being OPEN, this run continues
autonomously into the next cycle of #148.

Pick: the multi-node cost-graph composite-builder (decision E). The
"natural next" recorded consistently across cycles 2 and 3, and the next
item in the milestone breakdown order (cost-node abstraction → cost
nodes/wiring → ... → conviction axis). It is ripe now: three concrete
CostNodes + CostSum exist, and the CLI hand-wires them with a manual,
slot-indexed block (CostSum::builder(n) + a slot counter +
COST_SUM_PORTS[slot * COST_WIDTH + f] + a hardcoded MAX_RUN_COST_NODES
cap, crates/aura-cli/src/main.rs:2750-2780). That is real existing
wiring to encapsulate — not an absent consumer, so no speculative
generality.

Routing: spec-driven (specify direct-entry), not brainstorm. The design
is settled in sources: the cost-graph composite-builder in aura-composites
is named in this issue's scope; the aura-composites builder idiom is mature
(vol_stop / RiskExecutor: GraphBuilder::newinput_role
add/connect/feedexposebuildComposite); and the one real
fork — forwarding each cost node's heterogeneous extra_inputs() (e.g.
VolSlippage's volatility) across the composite boundary — is derivable from
the CostNode::extra_inputs() surface + the input_role/expose idiom
(namespaced composite input roles). No fork hangs on pure user preference;
specify Step 1.5 derives the rest and records them here.

Acceptance: behaviour-preserving. The composite inlines at bootstrap
(C11) to the same flat graph the manual CLI block produces, so the existing
suites + the C18 no-cost golden + the two cycle-3 net_expectancy_r goldens
(stage1_r_flat_cost / stage1_r_composed_cost) stay green verbatim;
byte-identity of net_expectancy_r is the regression net.

Session anchor: main fc52b4f (cycle-3 close, pushed).

## Cycle 4 (cost-graph composite-builder) — kickoff (boss, autonomous) `/boss mach weiter` after the cycle-3 push. Per the milestone-stop-policy (the milestone is the stop-marker — run cycles toward it without a per-cycle bounce-back) and this milestone being OPEN, this run continues autonomously into the next cycle of #148. **Pick: the multi-node cost-graph composite-builder (decision E).** The "natural next" recorded consistently across cycles 2 and 3, and the next item in the milestone breakdown order (cost-node abstraction → cost nodes/wiring → ... → conviction axis). It is ripe now: three concrete CostNodes + CostSum exist, and the CLI hand-wires them with a manual, slot-indexed block (`CostSum::builder(n)` + a `slot` counter + `COST_SUM_PORTS[slot * COST_WIDTH + f]` + a hardcoded `MAX_RUN_COST_NODES` cap, `crates/aura-cli/src/main.rs:2750-2780`). That is real existing wiring to encapsulate — not an absent consumer, so no speculative generality. **Routing: spec-driven (specify direct-entry), not brainstorm.** The design is settled in sources: the cost-graph composite-builder in `aura-composites` is named in this issue's scope; the `aura-composites` builder idiom is mature (`vol_stop` / `RiskExecutor`: `GraphBuilder::new` → `input_role` → `add`/`connect`/`feed` → `expose` → `build` → `Composite`); and the one real fork — forwarding each cost node's heterogeneous `extra_inputs()` (e.g. VolSlippage's `volatility`) across the composite boundary — is derivable from the `CostNode::extra_inputs()` surface + the `input_role`/`expose` idiom (namespaced composite input roles). No fork hangs on pure user preference; specify Step 1.5 derives the rest and records them here. **Acceptance: behaviour-preserving.** The composite inlines at bootstrap (C11) to the same flat graph the manual CLI block produces, so the existing suites + the C18 no-cost golden + the two cycle-3 `net_expectancy_r` goldens (`stage1_r_flat_cost` / `stage1_r_composed_cost`) stay green verbatim; byte-identity of `net_expectancy_r` is the regression net. Session anchor: main `fc52b4f` (cycle-3 close, pushed).
Author
Owner

Design record (specify, cycle 4 — cost-graph composite-builder)

Spec: 0084-cost-graph-composite. All forks below are derived orchestrator
decisions (rationale, not user provenance) under the bold Step-1.5 stance; the
sources (#148 scope + the mature aura-composites idiom + the CostNode
contract) settle them. None hangs on pure user preference, so none bounces.

  • Fork: composite API shape → a pub fn returning Composite taking the
    active cost nodes as Vec<PrimitiveBuilder>.
    Basis: derived. The two precedents in aura-composites are both
    pub-fn-returning-Composite (vol_stop, risk_executor); the cost-node set
    is known at construction, so no stateful builder struct is warranted.
    risk_executor's closure pattern (Box<dyn FnOnce(&mut GraphBuilder) -> NodeHandle>) exists only because its stop arms are heterogeneous constructs
    (a FixedStop primitive vs a vol_stop composite); cost nodes are uniformly
    PrimitiveBuilders (every cost node is a primitive via cost_node_builder),
    so a plain Vec<PrimitiveBuilder> is strictly simpler and sufficient.

  • Fork: extra-input forwarding / namespacing → the composite introspects
    each cost node's schema() and exposes its extra inputs (the ports beyond the
    4-wide geometry prefix) as composite input roles named cost[k].<port>.
    Basis: derived. The cost_node_builder contract guarantees geometry-first
    then extras (tested: cost_node_builder_assembles_geometry_prefix_then_extras),
    so schema.inputs[GEOMETRY_WIDTH..] is the extra-port set. The cost[k].<port>
    scheme mirrors CostSum's own cost[k].<field> input naming (cost_sum.rs:45)
    — one consistent cost[k].* vocabulary across the aggregator and the
    composite that wraps it. Index-namespacing (not label-namespacing) because
    labels can collide (two ConstantCosts) and are non-load-bearing debug
    symbols (C23).

  • Fork: GEOMETRY_WIDTH visibility → re-export GEOMETRY_WIDTH from
    aura-std's lib.rs (it is already pub in cost.rs, just not re-exported).
    Basis: derived. aura-composites is the first cross-crate consumer that must
    know where a cost node's extra inputs begin; re-exporting is demand-driven
    (the cost contract's public boundary now has a real consumer), not speculative
    surface. The alternative — a bespoke extra_ports(&PrimitiveBuilder) helper —
    adds API for what one already-pub constant + the tested geometry-prefix
    contract express directly.

  • Fork: CLI consumptionstage1_r_graph builds the active cost nodes into
    a Vec<PrimitiveBuilder> (same conditional order as today's manual block),
    adds cost_graph(nodes), wires the executor's geometry outputs to the
    composite's closed/open/entry_price/stop_price roles, feeds the vol proxy to
    the vol node's cost[k].volatility role, and reads the composite's aggregate
    3-field output into net_r_equity + the cost recorder. The hardcoded
    MAX_RUN_COST_NODES = 2 cap + the interned COST_SUM_PORTS slot table are
    deleted (the composite handles any arity; the vol proxy stays outside the
    composite, shared with the stop rule).
    Basis: mechanical consequence of the above; the CLI tracks each node's index
    exactly as it tracks slot today.

Acceptance — behaviour-preserving at the value level. The composite inlines
at bootstrap (C11) to the same flat computation (same cost nodes, edges, params)
the manual block produces, so every value output is byte-identical: the two
cycle-3 net_expectancy_r goldens, the C18 no-cost golden, and the full suite
stay green verbatim. Node labels / graph-debug paths may change (now nested under
cost_graph) — permitted by C23 (label is a non-load-bearing debug symbol); if
any test pins a with-cost graph label/shape, that is a ratified C23 label
change, not a regression (the grounding-check confirms whether one exists).

## Design record (specify, cycle 4 — cost-graph composite-builder) Spec: `0084-cost-graph-composite`. All forks below are **derived** orchestrator decisions (rationale, not user provenance) under the bold Step-1.5 stance; the sources (#148 scope + the mature `aura-composites` idiom + the `CostNode` contract) settle them. None hangs on pure user preference, so none bounces. - **Fork: composite API shape** → a `pub fn` returning `Composite` taking the active cost nodes as `Vec<PrimitiveBuilder>`. Basis: derived. The two precedents in `aura-composites` are both pub-fn-returning-`Composite` (`vol_stop`, `risk_executor`); the cost-node set is known at construction, so no stateful builder struct is warranted. `risk_executor`'s closure pattern (`Box<dyn FnOnce(&mut GraphBuilder) -> NodeHandle>`) exists only because its stop arms are heterogeneous *constructs* (a `FixedStop` primitive vs a `vol_stop` composite); cost nodes are uniformly `PrimitiveBuilder`s (every cost node is a primitive via `cost_node_builder`), so a plain `Vec<PrimitiveBuilder>` is strictly simpler and sufficient. - **Fork: extra-input forwarding / namespacing** → the composite introspects each cost node's `schema()` and exposes its extra inputs (the ports beyond the 4-wide geometry prefix) as composite input roles named `cost[k].<port>`. Basis: derived. The `cost_node_builder` contract guarantees geometry-first then extras (tested: `cost_node_builder_assembles_geometry_prefix_then_extras`), so `schema.inputs[GEOMETRY_WIDTH..]` is the extra-port set. The `cost[k].<port>` scheme mirrors `CostSum`'s own `cost[k].<field>` input naming (cost_sum.rs:45) — one consistent `cost[k].*` vocabulary across the aggregator and the composite that wraps it. Index-namespacing (not label-namespacing) because labels can collide (two `ConstantCost`s) and are non-load-bearing debug symbols (C23). - **Fork: GEOMETRY_WIDTH visibility** → re-export `GEOMETRY_WIDTH` from `aura-std`'s `lib.rs` (it is already `pub` in `cost.rs`, just not re-exported). Basis: derived. `aura-composites` is the first cross-crate consumer that must know where a cost node's extra inputs begin; re-exporting is demand-driven (the cost contract's public boundary now has a real consumer), not speculative surface. The alternative — a bespoke `extra_ports(&PrimitiveBuilder)` helper — adds API for what one already-`pub` constant + the tested geometry-prefix contract express directly. - **Fork: CLI consumption** → `stage1_r_graph` builds the active cost nodes into a `Vec<PrimitiveBuilder>` (same conditional order as today's manual block), adds `cost_graph(nodes)`, wires the executor's geometry outputs to the composite's `closed/open/entry_price/stop_price` roles, feeds the vol proxy to the vol node's `cost[k].volatility` role, and reads the composite's aggregate 3-field output into `net_r_equity` + the cost recorder. The hardcoded `MAX_RUN_COST_NODES = 2` cap + the interned `COST_SUM_PORTS` slot table are deleted (the composite handles any arity; the vol proxy stays outside the composite, shared with the stop rule). Basis: mechanical consequence of the above; the CLI tracks each node's index exactly as it tracks `slot` today. **Acceptance — behaviour-preserving at the value level.** The composite inlines at bootstrap (C11) to the same flat computation (same cost nodes, edges, params) the manual block produces, so every *value* output is byte-identical: the two cycle-3 `net_expectancy_r` goldens, the C18 no-cost golden, and the full suite stay green verbatim. Node labels / graph-debug paths may change (now nested under `cost_graph`) — permitted by C23 (label is a non-load-bearing debug symbol); if any test pins a with-cost graph *label/shape*, that is a ratified C23 label change, not a regression (the grounding-check confirms whether one exists).
Author
Owner

Cycle 4 (0084) — cost-graph composite-builder (decision E) — CLOSED, drift-clean

Commits (above anchor fc52b4f, unpushed): 9fb3d8b spec (boss-signed) →
550d5ce plan → d5c44dd feat → face187 audit.

Decision E ships. A new cost_graph(Vec<PrimitiveBuilder>) -> Composite in
aura-composites is the cost-model graph's authoring primitive: it fans the 4
PM-geometry inputs to N cost nodes, surfaces each node's extra inputs (discovered
via schema().inputs[GEOMETRY_WIDTH..]; GEOMETRY_WIDTH re-exported from aura-std)
as cost[k].<port> composite roles, sums them through CostSum, and exposes the
3-field aggregate. The CLI's manual slot-indexed cost-wiring and the hardcoded
MAX_RUN_COST_NODES = 2 cap are deleted
— the composite handles arbitrary arity.

Behaviour-preserving (C11): the composite inlines at bootstrap to the same flat
fan-in the hand-wired block produced, so the cycle-3 net_expectancy_r goldens
(-614.3134020253314 flat, -615.0304388396047 composed) and the C18 no-cost golden
stay byte-identical. Four aura-composites unit tests pin the exposed role-set + output
triple, including an n=3 arbitrary-arity fixture (two VolSlippageCost → distinct
cost[1]/cost[2].volatility) that witnesses the per-node namespacing the retired
2-node cap could not. Honours C9 / C16 / C23.

Fork decisions (all derived, logged above): composite API = a pub fn taking
Vec<PrimitiveBuilder> (vol_stop/risk_executor idiom); extra-input forwarding via
cost[k].<port> roles from schema introspection; GEOMETRY_WIDTH re-export
(demand-driven, first cross-crate consumer); the .leak() of runtime port names.

Audit: drift_found, every item dispositioned. Resolved in face187: the C10
ledger note (decision E shipped), the cost_graph .leak() provenance (the
"risk_executor precedent" was imprecise — that leak is in risk_executor's test,
one-shot/leak-safe; the production reuse pattern is the COL_PORTS intern), and a
stale cli_run.rs golden docstring. Filed #152 for the coupled deferred debt:
the cost[k].<port> namespacing restated across CostSum / cost_graph / the CLI
(a build-validated lockstep) and the per-build .leak() that becomes an allocation
regression on the per-member sweep path — both harmless on the current run-only cost
path, best fixed as one sweep-safe single-source cost-port contract with the
sweep-cost cycle (#152 depends on it).

Verified: cargo test --workspace 0 failures, clippy --workspace --all-targets -D warnings clean, cargo doc clean.

Still deferred (each its own cycle, recorded): data-grounded nodes (recorded-rate →
swap); per-cycle-held accrual (carry / funding — forces a summarize_r fold change);
the conviction-weighting R-aggregation axis (tdd-runnable); sweep-path / OOS-pooling
cost (now also gated by #152).

Milestone "Cost-model graph (in R)" remains OPEN.

## Cycle 4 (0084) — cost-graph composite-builder (decision E) — CLOSED, drift-clean Commits (above anchor `fc52b4f`, unpushed): `9fb3d8b` spec (boss-signed) → `550d5ce` plan → `d5c44dd` feat → `face187` audit. Decision E ships. A new `cost_graph(Vec<PrimitiveBuilder>) -> Composite` in **aura-composites** is the cost-model graph's authoring primitive: it fans the 4 PM-geometry inputs to `N` cost nodes, surfaces each node's extra inputs (discovered via `schema().inputs[GEOMETRY_WIDTH..]`; `GEOMETRY_WIDTH` re-exported from aura-std) as `cost[k].<port>` composite roles, sums them through `CostSum`, and exposes the 3-field aggregate. The CLI's manual slot-indexed cost-wiring **and the hardcoded `MAX_RUN_COST_NODES = 2` cap are deleted** — the composite handles arbitrary arity. **Behaviour-preserving** (C11): the composite inlines at bootstrap to the same flat fan-in the hand-wired block produced, so the cycle-3 `net_expectancy_r` goldens (`-614.3134020253314` flat, `-615.0304388396047` composed) and the C18 no-cost golden stay byte-identical. Four aura-composites unit tests pin the exposed role-set + output triple, including an n=3 arbitrary-arity fixture (two VolSlippageCost → distinct `cost[1]`/`cost[2].volatility`) that witnesses the per-node namespacing the retired 2-node cap could not. Honours C9 / C16 / C23. Fork decisions (all derived, logged above): composite API = a `pub fn` taking `Vec<PrimitiveBuilder>` (vol_stop/risk_executor idiom); extra-input forwarding via `cost[k].<port>` roles from schema introspection; `GEOMETRY_WIDTH` re-export (demand-driven, first cross-crate consumer); the `.leak()` of runtime port names. **Audit:** drift_found, every item dispositioned. Resolved in `face187`: the C10 ledger note (decision E shipped), the cost_graph `.leak()` provenance (the "risk_executor precedent" was imprecise — that leak is in risk_executor's *test*, one-shot/leak-safe; the production reuse pattern is the `COL_PORTS` intern), and a stale `cli_run.rs` golden docstring. **Filed #152** for the coupled deferred debt: the `cost[k].<port>` namespacing restated across `CostSum` / `cost_graph` / the CLI (a build-validated lockstep) and the per-build `.leak()` that becomes an allocation regression on the per-member sweep path — both harmless on the current run-only cost path, best fixed as one sweep-safe single-source cost-port contract **with** the sweep-cost cycle (#152 depends on it). Verified: `cargo test --workspace` 0 failures, `clippy --workspace --all-targets -D warnings` clean, `cargo doc` clean. Still deferred (each its own cycle, recorded): data-grounded nodes (recorded-rate → swap); per-cycle-held accrual (carry / funding — forces a `summarize_r` fold change); the conviction-weighting R-aggregation axis (tdd-runnable); sweep-path / OOS-pooling cost (now also gated by #152). Milestone "Cost-model graph (in R)" remains OPEN.
Author
Owner

Cycle 5 design decision (#148 — Cost-model graph in R): per-cycle-held accrual (approach B)

Fork: per-cycle-held accrual fold — A (accrue-to-close lump) vs B (honest per-held-cycle bleed).
B. Basis: user decision in the in-context discussion, verbatim "ja, mach B" (2026-06-28), after the A/B trade-off was laid out — A lumps the accrued cost onto the close row (summarize_r unchanged, but the net-R equity curve shows a step, not a bleed); B emits the accrual on each held cycle and sums it over the hold (an honest, continuously-bleeding curve, at the cost of a summarize_r fold change). The scalar net_expectancy_r is identical under A and B; only the equity-curve shape differs, and the user chose the honest curve.

Fork (derived): first-cycle scope — full calendar-aware overnight swap vs minimal accrual skeleton + a constant carry node.
Minimal: ship the accrual mechanism + a constant per-held-cycle CarryCost node; defer the calendar-aware overnight swap. Basis: derived — minimal-first-cycle discipline + the C10 "don't over-model" stance. The mechanism B requires is (1) a per-held-cycle charge mode on the cost-node skeleton (charge while held, not only at close, accumulating each held cycle) and (2) a summarize_r fold that sums the per-cycle cost over each trade's held span. That span-sum equals the existing single close-row charge for the per-close model (held rows charge 0, so Σ cost_in_r = close charge, exactly), so the cycle-0083/0084 net_expectancy_r goldens stay byte-identical — they are the refactor's regression net. The simplest node that exercises the mechanism is a constant per-held-cycle carry: a ConstantCost twin whose only difference is the per-held-cycle charge mode (a labelled stress parameter), wired through the existing cost_graph/CostSum aggregation (cycle 0084) with no new aggregation surface — CostSum already sums per-field across nodes, so a per-close ConstantCost and a per-cycle CarryCost compose correctly in one run.

Deferred to follow-up cycles, each its own unit:

  • a notional-based carry (price × rate, reads the price tap, R-normalized through the stop distance) — the realistic financing shape, vs the flat price-unit stress parameter shipped here;
  • the calendar-aware overnight swap proper — rollover-boundary timing (charge once per night, not once per cycle), the 3× Wednesday rule, and long/short rate asymmetry — which needs session/calendar metadata beside the hot path (domain invariant 4) and is, per C10, deploy-edge realism of low research value. A flat per-held-cycle carry stress parameter is the research-useful form.

Entry path: spec-driven (specify → planner). The high-level fork is user-settled; the remaining decisions (charge-mode on the CostNode trait vs a second runner type; the span-sum fold restructure; CarryCost as a ConstantCost twin; the byte-identity anchors) are all derivable from the existing cost.rs skeleton + summarize_r + the C10 contract. Not a one-assertion tdd item — it touches a load-bearing fold + a new trait method + a node + a CLI flag under a byte-identity constraint.

Session anchor: main 39f9387 (0084 close + glossary tap). Cycle-5 commits ride above it.

## Cycle 5 design decision (#148 — Cost-model graph in R): per-cycle-held accrual (approach B) **Fork: per-cycle-held accrual fold — A (accrue-to-close lump) vs B (honest per-held-cycle bleed).** → **B.** Basis: user decision in the in-context discussion, verbatim "ja, mach B" (2026-06-28), after the A/B trade-off was laid out — A lumps the accrued cost onto the close row (`summarize_r` unchanged, but the net-R equity curve shows a step, not a bleed); B emits the accrual on each held cycle and sums it over the hold (an honest, continuously-bleeding curve, at the cost of a `summarize_r` fold change). The scalar `net_expectancy_r` is identical under A and B; only the equity-curve shape differs, and the user chose the honest curve. **Fork (derived): first-cycle scope — full calendar-aware overnight swap vs minimal accrual skeleton + a constant carry node.** → **Minimal: ship the accrual *mechanism* + a constant per-held-cycle `CarryCost` node; defer the calendar-aware overnight swap.** Basis: derived — minimal-first-cycle discipline + the C10 "don't over-model" stance. The mechanism B requires is (1) a per-held-cycle charge mode on the cost-node skeleton (charge while held, not only at close, accumulating each held cycle) and (2) a `summarize_r` fold that sums the per-cycle cost over each trade's held span. That span-sum equals the existing single close-row charge for the per-close model (held rows charge 0, so `Σ cost_in_r = ` close charge, exactly), so the cycle-0083/0084 `net_expectancy_r` goldens stay byte-identical — they are the refactor's regression net. The simplest node that exercises the mechanism is a constant per-held-cycle carry: a `ConstantCost` twin whose only difference is the per-held-cycle charge mode (a labelled stress parameter), wired through the existing `cost_graph`/`CostSum` aggregation (cycle 0084) with no new aggregation surface — `CostSum` already sums per-field across nodes, so a per-close `ConstantCost` and a per-cycle `CarryCost` compose correctly in one run. Deferred to follow-up cycles, each its own unit: - a notional-based carry (`price × rate`, reads the price tap, R-normalized through the stop distance) — the realistic financing shape, vs the flat price-unit stress parameter shipped here; - the *calendar-aware* overnight swap proper — rollover-boundary timing (charge once per night, not once per cycle), the 3× Wednesday rule, and long/short rate asymmetry — which needs session/calendar metadata beside the hot path (domain invariant 4) and is, per C10, deploy-edge realism of low research value. A flat per-held-cycle carry stress parameter is the research-useful form. **Entry path:** spec-driven (specify → planner). The high-level fork is user-settled; the remaining decisions (charge-mode on the `CostNode` trait vs a second runner type; the span-sum fold restructure; `CarryCost` as a `ConstantCost` twin; the byte-identity anchors) are all derivable from the existing `cost.rs` skeleton + `summarize_r` + the C10 contract. Not a one-assertion `tdd` item — it touches a load-bearing fold + a new trait method + a node + a CLI flag under a byte-identity constraint. Session anchor: main `39f9387` (0084 close + glossary `tap`). Cycle-5 commits ride above it.
Author
Owner

Cycle 5 mechanism refinement (specify Step 1.5, derived) — same outcome, cleaner mechanism

Grounding cost.rs + summarize_r while writing the spec surfaced a cleaner mechanism for B than the one sketched above. The chosen outcome is unchanged — the honest, continuously-bleeding net-R equity curve the user picked ("ja, mach B"). Only the mechanism changes, and it is strictly simpler and lower-risk, so I am deciding it (derived) and recording it here for veto.

Refined mechanism. The bleed lives in open_cost_in_r (grown each held cycle = the open position's accrued-so-far, a mark-to-market term), while cost_in_r realizes the accrued total into cum at close. The net_r_equity tap already subtracts both cum_cost_in_r and open_cost_in_r, so the curve bleeds continuously during the hold via the growing open_cost_in_r, then cum picks the total up at close and open_cost_in_r drops to 0 — no double-count. Worked for a mixed run (commission AtClose c + carry PerHeldCycle 0.1, open t1–t3, close t4):

  • t1 open: cum_cost 0, open_cost c+0.1 → net subtracts commission(would-be) + 1 night carry
  • t2 open: cum_cost 0, open_cost c+0.2 → bleeds 0.1 / cycle
  • t3 open: cum_cost 0, open_cost c+0.3
  • t4 close: cum_cost c+0.4, open_cost 0 → cum takes the total, open hands off cleanly

Consequence — summarize_r is UNCHANGED (and so is the CLI net_eq wiring). The closed-trade read at the close row sees cost_in_r = the accrued total (the dump), so the per-trade cost is correct with no span-sum fold; the window-end open-trade read sees open_cost_in_r = accrued-so-far. The 3-field record's existing semantics generalize cleanly: cost_in_r = cost realized this cycle (into cum); open_cost_in_r = the open position's cost MtM as-of-now (AtClose: would-be close cost; PerHeldCycle: accrued-so-far). All cycle-0083/0084 goldens stay byte-identical trivially (summarize_r untouched + the AtClose runner branch kept verbatim).

Why this supersedes the "emit cost_in_r per held cycle + summarize_r span-sum fold" mechanism I first noted. That alternative changes the window-end open-trade fold (close-row read → span-sum), which would flip the AtClose window-end open trade from its would-be close cost to 0 — a byte-identity break if any cost golden's run ends with an open position. It is riskier and more complex for the same observable curve. Rejected on substance (lower risk + simpler + byte-clean), not effort.

Net cycle-5 change set (smaller than first scoped): CostNode::charge_mode() + the PerHeldCycle runner branch (accrue into acc, dump at close, grow open_cost_in_r); a CarryCost node (ConstantCost twin, PerHeldCycle); a --carry-per-cycle CLI flag. summarize_r and the CLI net_eq wiring are untouched. Load-bearing B-proof tests: a CostRunner unit test asserting open_cost_in_r GROWS across consecutive held cycles and cost_in_r dumps the total at close (this is what distinguishes B from A — the scalar net_expectancy_r is identical under both), plus a CLI --carry-per-cycle net_expectancy_r golden and an assertion that the net_r_equity tap bleeds during the hold, not only at close.

## Cycle 5 mechanism refinement (specify Step 1.5, derived) — same outcome, cleaner mechanism Grounding `cost.rs` + `summarize_r` while writing the spec surfaced a cleaner mechanism for **B** than the one sketched above. The chosen **outcome is unchanged** — the honest, continuously-bleeding net-R equity curve the user picked ("ja, mach B"). Only the mechanism changes, and it is strictly simpler and lower-risk, so I am deciding it (derived) and recording it here for veto. **Refined mechanism.** The bleed lives in `open_cost_in_r` (grown each held cycle = the open position's accrued-so-far, a mark-to-market term), while `cost_in_r` realizes the accrued total into `cum` at close. The `net_r_equity` tap already subtracts both `cum_cost_in_r` and `open_cost_in_r`, so the curve bleeds continuously during the hold via the growing `open_cost_in_r`, then `cum` picks the total up at close and `open_cost_in_r` drops to 0 — no double-count. Worked for a mixed run (commission AtClose `c` + carry PerHeldCycle `0.1`, open t1–t3, close t4): - t1 open: cum_cost 0, open_cost `c+0.1` → net subtracts commission(would-be) + 1 night carry - t2 open: cum_cost 0, open_cost `c+0.2` → bleeds 0.1 / cycle - t3 open: cum_cost 0, open_cost `c+0.3` - t4 close: cum_cost `c+0.4`, open_cost 0 → cum takes the total, open hands off cleanly **Consequence — `summarize_r` is UNCHANGED** (and so is the CLI `net_eq` wiring). The closed-trade read at the close row sees `cost_in_r` = the accrued total (the dump), so the per-trade cost is correct with no span-sum fold; the window-end open-trade read sees `open_cost_in_r` = accrued-so-far. The 3-field record's existing semantics generalize cleanly: `cost_in_r` = cost realized this cycle (into cum); `open_cost_in_r` = the open position's cost MtM as-of-now (AtClose: would-be close cost; PerHeldCycle: accrued-so-far). All cycle-0083/0084 goldens stay byte-identical **trivially** (`summarize_r` untouched + the AtClose runner branch kept verbatim). **Why this supersedes the "emit cost_in_r per held cycle + summarize_r span-sum fold" mechanism I first noted.** That alternative changes the window-end open-trade fold (close-row read → span-sum), which would flip the AtClose window-end open trade from its would-be close cost to 0 — a byte-identity break if any cost golden's run ends with an open position. It is riskier and more complex for the *same observable curve*. Rejected on substance (lower risk + simpler + byte-clean), not effort. **Net cycle-5 change set (smaller than first scoped):** `CostNode::charge_mode()` + the `PerHeldCycle` runner branch (accrue into `acc`, dump at close, grow `open_cost_in_r`); a `CarryCost` node (ConstantCost twin, PerHeldCycle); a `--carry-per-cycle` CLI flag. `summarize_r` and the CLI `net_eq` wiring are untouched. Load-bearing B-proof tests: a `CostRunner` unit test asserting `open_cost_in_r` GROWS across consecutive held cycles and `cost_in_r` dumps the total at close (this is what distinguishes B from A — the scalar `net_expectancy_r` is identical under both), plus a CLI `--carry-per-cycle` `net_expectancy_r` golden and an assertion that the `net_r_equity` tap bleeds during the hold, not only at close.
Author
Owner

Cycle 5 (0085) — per-cycle-held accrual + CarryCost — CLOSED, drift-clean

Commits (above anchor 39f9387, unpushed): 19ba1ec spec (boss-signed) → 2a8ee86 plan → f5e00a9 feat → 84ec2ab audit.

C10's "per-cycle-held factors accrue over the hold" clause is first realized. A cost factor now declares when it charges via ChargeMode { AtClose, PerHeldCycle } (a defaulted CostNode::charge_mode()), read by the one shared CostRunner — not a second runner type. The PerHeldCycle arm accrues per into a per-position acc each held cycle, dumps the accrued total into cum at close, and grows open_cost_in_r. The first accrual node, CarryCost (a ConstantCost twin differing only in charge_mode(), a labelled stress parameter), is wired through the existing cost_graph/CostSum via a run-path --carry-per-cycle flag — per-close and per-held-cycle nodes compose, the costs summing per-field (the composed golden is exactly additive).

Approach B (honest bleed) achieved with no summarize_r fold change (the mechanism refinement decided + logged above): the bleed lives in open_cost_in_r, which the net_r_equity tap already subtracts, so summarize_r and the CLI net_eq wiring are untouched and the cycle-0083/0084 net_expectancy_r goldens stay byte-identical (the AtClose arm is the pre-cycle-5 tokens verbatim). Architect: drift-clean, no code drift, no double-count (cum/open mutually exclusive per cycle). The B-vs-A discriminator (a growing intra-hold open_cost_in_r, invisible to the scalar net_expectancy_r) is pinned by unit B-proofs + a net_r_equity-bleeds-over-the-hold integration test + three robustness tests (negative-rate exit-2, monotone-in-rate, zero-rate exactly-free). cargo test --workspace 0 failures, clippy clean.

Milestone checkpoint — the cost-model graph's core is substantively complete

The composable cost-model graph now spans both charge modes: per-close (ConstantCost, VolSlippageCost) and per-held-cycle (CarryCost), through the CostNode trait + CostRunner + CostSum + the cost_graph composite into the net_r_equity curve and the summarize_r net fold. That is the milestone's substance: "composable cost nodes producing the net-R curve."

Remaining items are refinements or separately-gated — each a new cycle needing direction, none a clear autonomous next:

  • notional-based carry (price × rate, reads the price tap) — the realistic financing shape vs the flat stress parameter shipped;
  • calendar-aware overnight swap (rollover-boundary timing, 3× Wednesday, long/short asymmetry) — flagged deploy-edge realism of low research value per C10's "don't over-model" stance;
  • sweep-path / OOS-pooling cost — gated by #152 (the cost[k].<port> lockstep + the per-build .leak() to intern);
  • conviction-weighting R-aggregation axis — the conditional-shape tercile diagnostic already exists; a scalar conviction-weighted headline was leaned toward dropping (low value);
  • data-grounded recorded-rate nodes — gated by the #124 recorded-rate seam.

Milestone remains OPEN. Closing it is a deliberate act needing its end-to-end milestone fieldtest green (the milestone-close gate). 9 commits unpushed (origin/main at fc52b4f, the 0083 close).

## Cycle 5 (0085) — per-cycle-held accrual + CarryCost — CLOSED, drift-clean Commits (above anchor `39f9387`, unpushed): `19ba1ec` spec (boss-signed) → `2a8ee86` plan → `f5e00a9` feat → `84ec2ab` audit. C10's **"per-cycle-held factors accrue over the hold"** clause is first realized. A cost factor now declares *when* it charges via `ChargeMode { AtClose, PerHeldCycle }` (a defaulted `CostNode::charge_mode()`), read by the one shared `CostRunner` — not a second runner type. The `PerHeldCycle` arm accrues `per` into a per-position `acc` each held cycle, dumps the accrued total into `cum` at close, and grows `open_cost_in_r`. The first accrual node, **`CarryCost`** (a `ConstantCost` twin differing only in `charge_mode()`, a labelled stress parameter), is wired through the existing `cost_graph`/`CostSum` via a run-path `--carry-per-cycle` flag — per-close and per-held-cycle nodes compose, the costs summing per-field (the composed golden is exactly additive). **Approach B (honest bleed) achieved with no `summarize_r` fold change** (the mechanism refinement decided + logged above): the bleed lives in `open_cost_in_r`, which the `net_r_equity` tap already subtracts, so `summarize_r` and the CLI `net_eq` wiring are untouched and the cycle-0083/0084 `net_expectancy_r` goldens stay byte-identical (the AtClose arm is the pre-cycle-5 tokens verbatim). Architect: drift-clean, no code drift, no double-count (`cum`/`open` mutually exclusive per cycle). The B-vs-A discriminator (a growing intra-hold `open_cost_in_r`, invisible to the scalar `net_expectancy_r`) is pinned by unit B-proofs + a `net_r_equity`-bleeds-over-the-hold integration test + three robustness tests (negative-rate exit-2, monotone-in-rate, zero-rate exactly-free). `cargo test --workspace` 0 failures, clippy clean. ### Milestone checkpoint — the cost-model graph's core is substantively complete The composable cost-model graph now spans **both charge modes**: per-close (`ConstantCost`, `VolSlippageCost`) and per-held-cycle (`CarryCost`), through the `CostNode` trait + `CostRunner` + `CostSum` + the `cost_graph` composite into the `net_r_equity` curve and the `summarize_r` net fold. That is the milestone's substance: "composable cost nodes producing the net-R curve." Remaining items are refinements or separately-gated — each a new cycle needing direction, none a clear autonomous next: - **notional-based carry** (`price × rate`, reads the price tap) — the realistic financing shape vs the flat stress parameter shipped; - **calendar-aware overnight swap** (rollover-boundary timing, 3× Wednesday, long/short asymmetry) — flagged deploy-edge realism of low research value per C10's "don't over-model" stance; - **sweep-path / OOS-pooling cost** — gated by #152 (the `cost[k].<port>` lockstep + the per-build `.leak()` to intern); - **conviction-weighting R-aggregation axis** — the conditional-shape tercile diagnostic already exists; a scalar conviction-weighted headline was leaned toward dropping (low value); - **data-grounded recorded-rate nodes** — gated by the #124 recorded-rate seam. Milestone remains **OPEN**. Closing it is a deliberate act needing its end-to-end milestone fieldtest green (the milestone-close gate). 9 commits unpushed (origin/main at `fc52b4f`, the 0083 close).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#148