From 29cdc8c218c7401be2e678dd1a4d79b4775fa29b Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 28 Jun 2026 11:51:56 +0200 Subject: [PATCH] =?UTF-8?q?design(C10):=20retire=20the=20realistic=20broke?= =?UTF-8?q?r=20=E2=80=94=20cost=20is=20a=20cost-model=20graph=20in=20R=20(?= =?UTF-8?q?gross=E2=86=92net),=20money=20to=20the=20live=20deploy=20edge?= =?UTF-8?q?=20(refs=20#116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworks the C10 contract per a ratified in-context design discussion (#116). The historical "realistic broker" with authored frictions is retired as horseshoe-throwing: real friction (slippage, swaps, spread) is not historically knowable; the only reliable friction is measured forward against a real broker. In its place: - Cost is a composable C9 graph of cost nodes, in R, that approximates (never claims) realism — generalizing/subsuming the scalar round_trip_cost. Output is a net-R equity curve (gross R = signal, net R = after cost). - Research is pure feed-forward: the Sizer and compounding leave the loop; the z^-1 fill-edge register and the flat-1R-vs-compounding structural axis are gone. Compounding/Kelly/drawdown are post-hoc transforms at the deploy/account layer. - Executor = stop-rule + position-management in R. size/volume is a deploy concept; the position-event table is demoted to a deploy/reconciliation artifact. - Conviction-based risk allocation survives as an R-aggregation axis (flat vs |bias|-weighted), distinct from the removed money-Sizer. - Money, a real broker, and cTrader Open API are a separate, later live/deploy edge (C11 record-then-replay; C13 frozen deploy) — the only ground truth. Stage-1 R realizations preserved as history; the #117 reframe's R spine survives, its Stage-2/currency/register/compounding portions superseded. --- docs/design/INDEX.md | 565 ++++++++++++++++++++++++++----------------- 1 file changed, 340 insertions(+), 225 deletions(-) diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index 4b1a2dc..7f8cc7c 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -477,7 +477,7 @@ distinguishability check, if ever wanted (e.g. for the WASM graph view's #21 thread), is decoupled from param-space injectivity. Construction-phase only; the flat graph stays name-free (C23). -### C10 — Strategy output is a bias stream; signal quality is measured in R; risk-based execution and realistic brokers are decoupled downstream layers +### C10 — Strategy output is a bias stream; signal quality is measured in R; cost is a composable downstream graph (gross R → net R); money is decoupled to the live deploy edge **Guarantee.** A strategy's primary, backtestable output is **not** an equity curve, **nor a position-event table**, **nor a position size**, but a **bias stream**: the DAG expresses exactly one *state* at time t (C8 — a node emits at @@ -488,245 +488,360 @@ case). Bias is **unsized**: position sizing and the protective stop do **not** live in the strategy. The chain is `signals (scores) → decision node → bias stream`. -**Risk-based execution is a decoupled downstream layer.** Turning a bias into a -position is the job of a **risk-based execution** chain — `bias → stop-rule → -Sizer → Veto → position-management` — never the strategy's. The **stop-rule** sets -a protective stop, which **defines the risk unit R** (1R = the loss taken if -stopped). The **Sizer** sizes the trade in R; the **Veto** is a distinct -pre-trade gate (position / exposure caps), kept separate from sizing. These wire -into a **RiskExecutor** composite (per symbol); RiskExecutors nest inside a -**Broker / Account** composite that aggregates per-symbol P&L into account equity. -The account **mode is a composition constraint**: a *netting* account allows ≤1 -RiskExecutor per (strategy, symbol); a *hedging* account (the European CFD -default) allows many per symbol, even oppositely directed. +**Risk-based execution is a decoupled downstream layer; in research it is Stop + +position-management in R, no Sizer.** Turning a bias into a tracked trade is the +job of a downstream **execution** chain, never the strategy's. The **stop-rule** +sets a protective stop, which **defines the risk unit R** (1R = the loss taken if +stopped). In the research loop the executor is **stop-rule → position-management**, +operating **directly in R**, with the **Veto** an optional documented pre-trade-gate +seam (a pass-through identity DCE'd away under C19/C23 when absent): there is **no +Sizer**. Sizing in *currency* (`size` / `volume`) is a **deploy** concept. C8's +wiring totality (cycle-0040 `check_ports_connected`: no optional-input concept — +every declared port is covered by exactly one wiring act) forbids a *dangling* +`size` port, so the resolution is concrete: research `PositionManagement` either +**drops its `size` input port** from its node signature, or has that port **driven +by a constant unit node** (the flat-1R degenerate) — never an unwired "vestigial" +port; the record's `size` field is held at unit and carries no research +information, and the position-event table's `volume` column likewise. Consequently +the **position-event table is demoted to a deploy / reconciliation artifact** (real +volume lives there), no longer a research artifact and no longer "fed to a broker". +The three-way decoupling of **direction (bias) / sizing / fill** stands as a +*structure*; sizing and fill are simply pushed entirely to the deploy edge, out of +the research loop. -**Signal quality is measured in R — Stage 1.** A downstream **R-evaluator** -consumes a RiskExecutor run under **flat-1R** sizing (every trade risks exactly -1R) and integrates the per-trade R-outcomes into an **R-expectancy** / R-curve: -the account- and instrument-agnostic yardstick for *"how much R out per 1R -risked?"*. Flat-1R needs no equity, so this stage is **feed-forward** (no -feedback, no cycle) — the primary research loop, maximally parallel and -deterministic (C1). R, not pips, is the unit: pips are not risk-normalized. +**Signal quality is measured in R — gross R and net R.** A downstream +**R-evaluator** consumes the executor run and integrates the per-trade R-outcomes +into an **R-expectancy** / R-curve: the account- and instrument-agnostic yardstick +for *"how much R out per 1R risked?"*. R, not pips, is the unit (pips are not +risk-normalized). Two readings of the same unit: **gross R** (signal only) vs **net +R** (after the cost model), with `net R = gross R − cost-in-R`. The headline +artifact is the **net-R equity curve** — the cost-drag drawn onto the R curve, +recorded through a named **`net_r_equity`** tap/sink (sibling of the existing +`r_equity` tap; a sink is the only thing the registry can display, C8/C18/C22). No +new unit is invented: it is R, gross and net, **continuous with the existing +`net_expectancy_r`**. A bare **gross-R run with no cost model attached is valid**: +the cost layer is optional and additively composed-on (the zero-cost baseline is +the "default simple" floor). -**Currency P&L is Stage 2 — deploy viability.** Entered only once `E[R] > 0` is -established. Here sizing is **fixed-fractional** (size = risk-fraction · equity / -stop-distance), so it **reads account equity** and **compounds**; **realistic -broker** nodes apply real spread / commission / slippage / lot / margin and emit a -**currency** equity stream. This introduces the *only* feedback in the whole -design — equity → Sizer — cut by a **`z⁻¹` register on the fill edge** (the -realized/fill component is delayed one cycle; mark-to-market off the current price -stays a legal same-cycle read, C2), **encapsulated inside the executor -composite**. Whether that register is live is the **flat-1R vs compounding -structural axis** (instantiating a feedback edge is topology, fixed at bootstrap -(C19), hence a structural axis (C20), never a numeric sweep param): flat-1R DCEs -it away (Stage 1), compounding keeps it (Stage 2). The explicit register is -**mandatory** — C9's "only feedback is an explicit delay node" made concrete — -because the bootstrap's optimiser reorders the flat graph (C23 CSE/DCE): the -implicit read-before-fill ordering by which conventional sequential backtesters -cut this loop would not survive that reordering. +**Cost is a COST MODEL — a composable C9 graph of cost nodes, in R, that +approximates (never claims) realism.** The realistic broker is *retired* (see +Forbids / the 2026-06-28 reframe): real friction — slippage (live +liquidity / order-size / volatility at fill), swaps (broker-set, time-varying), +even recorded feed spread (often a fake constant) — is **not historically +knowable**, so an authored-friction historical broker is "horseshoe-throwing". Its +replacement is a **cost model**: an ordinary downstream **C9 graph of cost nodes** +that *approximates* the cost side a broker would produce, explicitly as an +approximation. The cost nodes live in `aura-std` (the cost-graph composite-builder +in `aura-composites`), never in the domain-free `aura-engine` (C14/C16). They are +**not** "additive on the R stream" in isolation: a cost node **reads the state it +depends on** — the price stream, a realized-volatility tap, a C11-recorded +interest-rate source, and the executor's per-cycle R-record / trade events — and +emits a **cost-in-R** stream that is subtracted from gross R to yield net R. Cost +attaches at the structurally correct grain: **per-trade** factors (commission, a +flat cost-per-trade) deduct from `realized_r` at close; **per-cycle-held** factors +(carry / funding / swap) accrue over the holding duration. The model **generalizes** +the existing scalar `round_trip_cost` / `net_expectancy_r` into a possibly +**state-dependent graph**: the scalar `round_trip_cost` is the degenerate +constant-per-trade special case, **subsumed** by the cost graph — the post-run +`summarize_r` fold no longer recomputes cost independently but folds the +cost-model's net-R stream into `net_expectancy_r` (one home for cost, no +double-count). Discipline: every cost factor is **either** a clearly-labelled +**stress-parameter** (e.g. a flat cost-per-trade is a breakeven-threshold probe) +**or** **data-grounded / falsifiable** (e.g. realized-volatility → slippage; +recorded interest-rate data via C11 → funding / swap). **Default simple; +complexity is earned per grounded factor.** Stacking unfalsifiable guesses +(over-modelling) is the anti-pattern. -**The position-event table** — turning the executed book into a broker-independent, -time-ordered **table of position events** (pure scalar columns, C7: `event_ts: -timestamp`, `action: i64` buy / sell / close, `position_id: i64`, -`instrument_id: i64`, `volume: f64` unsigned — direction is the `action`; a -position's open time is its opening event's `event_ts`, no separate `open_ts`; a -`close` references a `position_id` and may be partial via its own `volume`) — is a -**decoupled, derived, downstream** Stage-2 layer. The events are the **first -difference of the book** under book-tracking (`deal = target − book − in_flight`; -a close sizes the actual book, never an exposure delta), materialized as a -*computed table* (multiple events may share one `event_ts` — e.g. a reversal's -close + open), **never as a per-`eval` node output** (C8 caps one record per -cycle). This table feeds the realistic broker nodes; one table feeds many, giving -directly comparable currency curves. Live: a realistic broker node consumes the -events in real time and routes orders as a side effect; reconciliation with the -real account is an external adapter. +**The research loop is pure feed-forward — compounding is removed.** Flat-1R-style +R accounting needs no equity, and with the Sizer gone there is **no equity → size +edge at all** in research: the loop is **feed-forward, maximally parallel and +deterministic (C1)**, the cost model a feed-forward subtraction on the R stream. +**Compounding is removed from research**: it is a **post-strategy money-management +transform** — a pure function of the per-trade **net-R sequence** and a +bet-fraction *f*, multiplicative and path / order-dependent, the **sole** source of +feedback — so compounding, Kelly-*f*, and drawdown-under-compounding are derived +**post-hoc, analytically, at the deploy / account layer** from the net-R +distribution. Consequently the `z⁻¹` fill-edge register **and** the +flat-1R-vs-compounding structural axis are **gone from the research loop** (with no +in-loop feedback there is nothing for a register to cut, so the C23-reordering +hazard the old text invoked to make the register mandatory no longer exists — this +is strictly C9 / C23-cleaner). + +**Conviction-based risk allocation survives — as an R-aggregation axis, not a +Sizer.** Scaling risk by bias strength is **signal-side and R-denominated**. +Because per-trade R is **size-invariant**, conviction cannot be expressed by +scaling position size (that is invisible to R); it is expressed by **weighting the +per-trade R-contribution** in the R-equity: **flat** (sum of `realized_r`, sign +only) vs **conviction-weighted** (sum of `|bias| · realized_r`, sign + magnitude). +This is a **feed-forward, additive, order-independent research axis** (flat vs +conviction-weighted R-aggregation), distinct from the removed money-Sizer; it is +tested via the existing `conviction_at_entry` field and `conviction_terciles_r` +metric, and may sit in-graph or as a post-hoc fold. + +**Money / real broker / cTrader Open API = a separate, later live / deploy-edge +concern — the only `belastbare` (reliable) ground truth, measured never modelled.** +Reliable friction statistics require **forward-trading against a real broker** +(e.g. cTrader Open API), and are non-stationary even then. This fits C11 +(record-then-replay: real fills are recorded live, then replayable) and the +frozen-deploy invariant (C13: deploy = frozen bot + broker connection); +reconciliation with the real account is an **external I/O adapter** at the +recording / deploy edge, not an in-graph node. **This is the only place account +money appears.** (Currency-denominated *reference geometry* — pip value, +stop-distance-in-currency from the C15 `instrument_geometry` sidecar — is still +read at the **ingestion** edge to normalize a currency / pip cost factor into R; +notional size cancels in `cost_in_R = cost_in_currency / (size · stop_dist)`, so +the cost model is R-pure without ever holding *equity*. It is equity / account +money, not reference geometry, that lives only at the deploy edge.) The +broker-independent **position-event table** (`event_ts, action[buy/sell/close], +position_id, instrument_id, volume`) is the **deploy / reconciliation** record at +this edge (real volume), not a research artifact. + +**Honesty principle.** The net-R curve under the cost model is a **research / +ranking tool and a hypothesis**; the **forward / live run against a real broker is +the ground truth**. The cost model *approximates*; it **never claims realism**. +`SimBroker` (the pip-equity, unsized-exposure node) is the **pre-reframe pip +ancestor** — with the net-R cost model it is **redundant as a quality measure** (R +supersedes pips). It is retained as a **legacy / simple optional pip yardstick** +(still wired in the `stage1-r` harness for an honest dual readout), **not part of +the new model and not to be expanded**. -So the layers attach at distinct points: the **R-evaluator** on a flat-1R -RiskExecutor (signal quality, R, Stage 1); **realistic brokers** on the derived -position-event table (execution viability, currency, Stage 2). All are ordinary -downstream nodes (C8/C9); several can attach at once for directly comparable -curves. **Forbids.** Putting **sizing or the stop in the strategy** (bias is unsized; the -Sizer and stop-rule own them); treating an equity curve as the strategy's output; -making the **position-event table the strategy's direct DAG output** (it is -derived, not emitted per `eval` — a decision instant may need >1 event, which C8 -forbids) or the measure of signal quality; **measuring signal quality in currency -before R is established** (Stage 2 before Stage 1); a **combinational equity → size -cycle** (the fill-edge register is mandatory — an implicit execution ordering does -not survive C23 reordering); **fusing the Sizer and the Veto** into one node (the -size / veto / fill roles stay distinct); baking a broker into the strategy; a -special external broker subsystem (a broker is an ordinary node); storing -`open_ts` (derive it from the opening event); a signed-volume direction trick in -the event table (use `action`); broker-specific assumptions leaking into the -strategy logic. +stop-rule owns the stop, R is the unit); treating an **equity curve** (R or +currency) as the strategy's direct output; **putting a Sizer / currency size / +`volume` into the research loop** (size is a deploy concept; research is in R); +leaving a **dangling `size` input port** on the research executor (C8 wiring +totality forbids it — drop the port or drive it with a constant unit); **any +equity → size / equity → anything feedback in research** (the research loop is pure +feed-forward; compounding is a post-hoc money-management transform, not an in-loop +edge); modelling an **authored-friction "realistic broker" over historical data** +(real friction is not historically knowable — use the approximating cost model, and +treat the real broker as the live-edge ground truth only); **claiming the cost +model is realism** (it is an explicit approximation); **stacking unfalsifiable cost +guesses** (each cost factor is a labelled stress-parameter *or* data-grounded — +over-modelling is the anti-pattern); **computing cost in two homes** (the cost +graph owns cost; the post-run fold subsumes the old scalar `round_trip_cost`, never +double-counts it); expressing **conviction by scaling position size** +(size-invisible to R; conviction is an R-aggregation weight); making the +**position-event table the strategy's direct DAG output** (it is derived, not +emitted per `eval` — a decision instant may need >1 event, which C8 forbids) or a +**research** measure of signal quality (it is a deploy / reconciliation artifact); +**measuring signal quality in currency / account money** in the research loop at all +(account money lives only at the live deploy edge; reference geometry at ingestion +is not account money); baking a broker into the strategy or an **in-graph broker +subsystem** (the in-graph realistic broker is retired; the only broker is the +live-edge I/O adapter, C11 / C13); a signed-volume direction trick in the event +table (use `action`); storing `open_ts` (derive it from the opening event). + **Why.** A strategy's edge is a *procedure*, not a currency outcome ("focus on the procedure, not the money"): the right primary question is *"how much R out per 1R risked?"*, and R — defined by the stop — is the only account- and instrument-agnostic, risk-normalized unit (pips are not). Separating **direction -(bias) from sizing (Sizer) from fill (broker)** is the decomposition every mature -system converges on (LEAN's Alpha → Portfolio-Construction → Execution is a near -isomorphism; backtrader, QSTrader, zipline all emit an unsized directional signal -sized downstream). The **Stage-1 / Stage-2 cleave** quarantines the *only* -feedback (equity → sizing) into Stage 2, leaving the signal-quality layer -feed-forward, parallel and deterministic (C1); its closest external precedent is -vectorbt's split of vectorized signal generation from sequential money-management -— it is aura's own synthesis, not an industry standard. The DAG holds exactly one -state at t and a node emits ≤1 record per `eval` (C8), so the faithful per-cycle -output is the **bias** (one value); position *events* are its book's first -difference, a derived consequence — decoupling them resolves the C8↔C10 impedance. -Modelling brokers and executors as nodes (not a bespoke subsystem) keeps them -within the one Node/graph abstraction (C9). This supersedes the **exposure** -framing (the strategy's output was a signed fractional *position*) of the -cycle-0007 reframe and its realization notes below; that in turn superseded the -still-earlier "the strategy's output is the position-event table" framing. +(bias) from sizing from fill** is the decomposition every mature system converges +on (LEAN's Alpha → Portfolio-Construction → Execution is a near isomorphism; +backtrader, QSTrader, zipline all emit an unsized directional signal sized +downstream) — and aura pushes *sizing* and *fill* off the research loop for two +**distinct** reasons: **sizing** is off because per-trade R is **size-invariant** +(size carries no information in R — flat-1R is perfectly knowable, it just does not +matter), and **fill / friction** is off because real friction is **not +historically knowable** and therefore not honest over history. Keeping research +**pure feed-forward** leaves the signal-quality layer parallel and deterministic +(C1); the **only** real feedback (equity → bet-fraction) is **compounding**, a +closed-form, path-dependent transform of the net-R sequence, and therefore belongs +**after** the strategy, at the deploy / account layer, not as an in-loop register. +The **cost model as a C9 graph** keeps cost within the one Node / graph abstraction +(C9) and generalizes the scalar `net_expectancy_r` continuously, while the +**gross-R / net-R** split states the cost-drag honestly without inventing a unit. +Refusing the historical realistic broker is an **honesty** stance: the only +`belastbare` friction is **measured forward** against a real broker (cTrader Open +API) — the live deploy edge, the sole place account money and ground truth appear. +The DAG holds exactly one state at t and a node emits ≤1 record per `eval` (C8), so +the faithful per-cycle output is the **bias** (one value); position *events* are a +derived, deploy-side consequence. This supersedes the **realistic-broker / +currency / compounding / Stage-1-vs-Stage-2** framing of the #117 reframe (see the +2026-06-28 reframe note), which itself superseded the **exposure** framing of +cycle-0007 and the still-earlier "strategy's output is the position-event table" +framing. + +**Reframe (2026-06-28, #116 — realistic broker retired; cost-model graph in R; money to the live edge).** +This is the live contract above. Ratified in an in-context design discussion +(reference issue #116). It **preserves** the durable spine — unsized bias stream +(sign = direction, magnitude = conviction), signal quality in R, the stop defining +R, and the decoupling of direction from sizing from fill — and the shipped Stage-1 +realizations (the `Bias` node, `FixedStop` / `vol_stop`, `PositionManagement`, +`summarize_r` / `RMetrics` / `sqn_normalized`, SQN as ranking objective). The +**RiskExecutor composite survives in shape** (bias + price → stop-rule → +position-management) but with its **Sizer interior removed** and its `risk_budget` +argument dropped / vestigial (it sized the now-removed Sizer); the Veto remains an +optional documented seam. The contract **supersedes** the following, which were +**design intent, largely unbuilt** (#116 — the realistic broker and the whole +Stage-2 currency layer were never implemented; the Stage-1 R chain was) and are +retired: +- the **"realistic broker"** concept — an authored-friction historical broker — + rejected as "horseshoe-throwing" (real friction is not historically knowable), + replaced by the **cost model**: a composable C9 graph of cost nodes (in + `aura-std` / `aura-composites`), approximating not claiming realism, + generalizing / subsuming `round_trip_cost` into `net_expectancy_r`; +- the **"Currency P&L is Stage 2"** paragraph in full, the **Stage-1-vs-Stage-2 + hard sequencing gate**, and **currency / fixed-fractional / compounding** in + research — compounding is now a post-hoc money-management transform of the + net-R sequence at the deploy / account layer; +- the **`z⁻¹` register on the fill edge** and the **flat-1R-vs-compounding + structural axis** as research mechanism — there is no equity → size edge in + research, so no register and no such axis in the loop; +- the **Sizer in research** and currency **size / `volume`** — size is a deploy + concept; the research executor is stop + position-management in R; + `PositionManagement`'s `size` port is dropped or constant-driven (no dangling + port, C8), its `size` field and the event table's `volume` are vestigial in + research; +- the **"a broker is an ordinary in-graph node / no special external broker + subsystem"** forbid, **for the live edge only**: in-graph brokers are retired + outright, and the live broker is now an explicit **I/O adapter** at the C11 + recording / C13 deploy edge — not an in-graph node and not part of the research + graph (the no-in-graph-broker-subsystem prohibition still holds inside the + graph); +- the **position-event table as the realistic-broker input** and its + first-difference-of-the-book (`deal = target − book − in_flight`) execution + framing — the table (schema 0063 #114, derive 0068 #115) **survives** as the + **deploy / reconciliation** artifact (real volume), not a research artifact and + not "fed to a broker" in research. +Money, a real broker, and cTrader Open API are a **separate, later live / +deploy-edge** concern (C11 record-then-replay; C13 frozen-deploy invariant) — the +only `belastbare` ground truth, measured, never modelled. The honesty principle is +explicit: the net-R curve is a research / ranking hypothesis; the forward / live +run is ground truth. `SimBroker` is downgraded to a legacy / optional pip +yardstick, not to be expanded. (Terminology note: with Stage 2 gone, the +**"Stage-1"** label below is no longer one half of a two-stage gate — it survives +only as a historical cycle / identifier name, like the `exposure` → `bias` on-disk +alias, denoting the shipped feed-forward R chain.) + **Reframe (2026-06-23, #117 — exposure → bias, R as the signal-quality unit).** -The contract above is the R-reframe; the realization notes below describe the -**pre-reframe code** (`Exposure`, `SimBroker`, pip-equity) and are retained as -history and as the **Stage-1 ancestor**: `Exposure { scale }` is the ancestor of -the unsized `bias` node, and `SimBroker`'s pip integral is the ancestor of the -R-evaluator (which additionally requires a stop, since R is stop-defined, so it -consumes a flat-1R RiskExecutor rather than raw bias). The `exposure → bias` -rename, the RiskExecutor / Sizer / Veto nodes, and the R-evaluator **landed in -cycle 0065** (Stage-1; see the realization note below); the position-event schema (0063, #114) survives -unchanged as the Stage-2 audit layer, though its derivation is now the **book** -first-difference (`deal = target − book − in_flight`), not the flawed 0064 -exposure-integral derive (abandoned). Industry grounding for this reframe: LEAN / -nautilus_trader / backtrader / QSTrader / vectorbt / zipline (see #117 decision -log). -**Realization (cycle 0007).** The signal-quality half of this contract is now -realized at the substrate, as two `aura-std` nodes composed on the unchanged -engine (the engine stays domain-free — it routes only `f64` records, never -"exposure" or "equity"). The **exposure stream** is realized as `Exposure { scale -}`: the decision/sizing node, `clamp(signal / scale, -1, +1)`, one `f64` per fired -cycle (`None` until warmed up). The **sim-optimal broker** is realized as -`SimBroker { pip_size }`: a two-input node (exposure, price) that accumulates -`prev_exposure · (price − prev_price) / pip_size` and emits cumulative pip equity -— the exposure held *into* a cycle (decided at t-1) earns that cycle's return, so -the integration is causal (C2), and `pip_size` is held reference metadata -(C7/C15), never streamed. An end-to-end harness (SMA-cross → `Exposure` → -`SimBroker` → recording sink) produces a recorded pip-equity curve, bit-identical -across runs (C1). The **position-management half** — deriving the position-event -table (buy/sell/close, `position_id`, partial closes) from the exposure history, -and the realistic broker nodes that consume it — is deliberately **not** built -this cycle; it remains the decoupled, derived, deferred layer described above. -**Realization (per-instrument pip channel, 2026-06, #22).** `SimBroker`'s -`pip_size` is now sourced **per instrument**, not from one global literal. The -divisor is resolved from the recorded geometry sidecar (`instrument_geometry`, over -data-server's `symbol_meta`), at the ingestion/source edge where the symbol still -exists (never `Aura.toml`, never `Ctx`); the engine stays domain-free (no instrument -identity reaches the hot path). (The original cycle-0022 form was a Rust-authored -vetted floor `InstrumentSpec { pip_size }` + `instrument_spec(symbol)`; cycle 0074 -removed that floor — see the C15 note below — once the sidecar geometry made it -redundant for the real path. Refuse-don't-guess on absent geometry.) -**Realization (position-event schema, cycle 0063, #114).** The position-management -half's **schema** now landed (its derivation and the brokers remain deferred): a -closed `PositionAction { Buy, Sell, Close }` enum + the `PositionEvent` row -(`event_ts`, `action`, `position_id`, `instrument_id`, unsigned `volume`; no -`open_ts`; direction *is* the action) live beside `RunMetrics` as a -post-run value type (not a per-`eval` node — C8) — both in the `aura-analysis` crate -since cycle 0079 (#136); originally `aura-engine`, see the C16 cycle-0079 note. `action` serde-encodes as a bare -`i64` (Buy=0, Sell=1, Close=2), the C7 scalar column form the ledger's table spec -requires, with an out-of-range code rejected on read. Still deferred: -`derive_position_events` (the first-difference reduction over the exposure history, -#115) and the realistic broker nodes that consume the table (#116). The table stays -broker-independent. -The honesty rule is **refuse, don't guess**: a real-data run for a symbol with no -vetted spec is a usage error (`exit 2`), so cross-asset pip equity is comparable by -construction rather than by researcher discipline. Threaded through the CLI -`aura run --real` path; the manifest broker label records the looked-up pip. The -runnable GER40 examples (already at the correct `1.0`) are routed through the same -lookup separately (#98). +[HISTORY — its R spine survives into the 2026-06-28 contract; its Stage-2 currency / +realistic-broker / register / flat-1R-vs-compounding portions are SUPERSEDED by that +reframe.] The contract was reframed from **exposure** (a signed fractional +position) to an **unsized bias** plus **R** as the signal-quality unit. The +realization notes below describe the **pre-reframe code** (`Exposure`, `SimBroker`, +pip-equity), retained as history and as the **ancestor** of the current chain: +`Exposure { scale }` is the ancestor of the unsized `bias` node, and `SimBroker`'s +pip integral is the ancestor of the R-evaluator (which additionally requires a +stop, since R is stop-defined). The `exposure → bias` rename, the RiskExecutor / +Sizer / Veto nodes, and the R-evaluator **landed in cycle 0065**; the +position-event schema (0063, #114) survives as the audit layer. Industry grounding +for this reframe: LEAN / nautilus_trader / backtrader / QSTrader / vectorbt / +zipline (see #117 decision log). + +**Realization (cycle 0007).** [HISTORY — pre-reframe; `SimBroker` is now legacy per +the 2026-06-28 reframe.] The signal-quality half was realized at the substrate as +two `aura-std` nodes on the unchanged engine (the engine stays domain-free — it +routes only `f64` records). The **exposure stream** was realized as `Exposure { +scale }`: `clamp(signal / scale, -1, +1)`, one `f64` per fired cycle. The +**sim-optimal broker** was realized as `SimBroker { pip_size }`: a two-input node +(exposure, price) accumulating `prev_exposure · (price − prev_price) / pip_size` +and emitting cumulative pip equity — the exposure held *into* a cycle (decided at +t-1) earns that cycle's return (causal, C2); `pip_size` is held reference metadata +(C7/C15). An end-to-end harness (SMA-cross → `Exposure` → `SimBroker` → recording +sink) produced a recorded pip-equity curve, bit-identical across runs (C1). + +**Realization (per-instrument pip channel, 2026-06, #22).** [HISTORY — pertains to +the legacy `SimBroker` pip channel.] `SimBroker`'s `pip_size` is sourced **per +instrument** from the recorded geometry sidecar (`instrument_geometry`, over +data-server's `symbol_meta`), at the ingestion / source edge (never `Aura.toml`, +never `Ctx`); the engine stays domain-free. (The original cycle-0022 form was a +Rust-authored vetted floor `InstrumentSpec { pip_size }` + `instrument_spec(symbol)`; +cycle 0074 removed that floor — see the C15 note — once the sidecar geometry made it +redundant for the real path. Refuse-don't-guess on absent geometry.) The honesty +rule is **refuse, don't guess**: a real-data run for a symbol with no vetted spec +is a usage error (`exit 2`). Threaded through the CLI `aura run --real` path; the +manifest broker label records the looked-up pip. + +**Realization (position-event schema, cycle 0063, #114).** [Survives as the +**deploy / reconciliation** schema per the 2026-06-28 reframe — no longer a +broker-input research artifact.] A closed `PositionAction { Buy, Sell, Close }` +enum + the `PositionEvent` row (`event_ts`, `action`, `position_id`, +`instrument_id`, unsigned `volume`; no `open_ts`; direction *is* the action) live +beside `RunMetrics` as a post-run value type (not a per-`eval` node — C8) — both in +the `aura-analysis` crate since cycle 0079 (#136); originally `aura-engine`, see the +C16 cycle-0079 note. `action` serde-encodes as a bare `i64` (Buy=0, Sell=1, +Close=2), the C7 scalar column form, with an out-of-range code rejected on read. +The table stays broker-independent. + **Realization (cycle 0065 — Stage-1 R signal quality, #119/#126/#127/#128/#129).** -The Stage-1 chain above is now built. `Exposure → Bias` renames the unsized strategy -output (the node + its output field); the persisted `exposure_sign_flips` metric key -(kept as a serde alias) and the `SimBroker` `exposure` input slot + the on-disk -`exposure` **tap** label (a deliberate permanent keep, #117) retain the old name. The -strategy-output **param namespace** — the `Bias` instance, its `bias.scale` knob, and -the `bias_scale` manifest param — was the deferred rename tail, **completed in #134** -(the knob path is a runtime address, so no on-disk back-compat break). A **stop-rule** -defines 1R: `FixedStop` (a triggered-constant primitive) and a `vol_stop(length, k)` -**composition** `k·√EMA(Δ²)` (the originally-fused node was corrected to a composition -of new `Mul`/`Sqrt` primitives — a node is a primitive only if not DAG-expressible -from others). **`PositionManagement`** (`aura-std`) is the stateful heart: it latches -the entry-cycle stop distance as the immutable R-denominator, marks against the +[The R spine here is the live model; the *Stage-2 deferral* clause at its end is +SUPERSEDED by the 2026-06-28 reframe — there is no Stage-2 currency / compounding +layer; cost is now the cost-model graph and money lives only at the live deploy +edge. "Stage-1" reads as a historical identifier, not a gate half.] `Exposure → +Bias` renamed the unsized strategy output (node + output field); the persisted +`exposure_sign_flips` metric key (serde alias), the `SimBroker` `exposure` input +slot, and the on-disk `exposure` **tap** label retain the old name. The +strategy-output **param namespace** (the `Bias` instance, its `bias.scale` knob, +the `bias_scale` manifest param) was completed in #134. A **stop-rule** defines 1R: +`FixedStop` (a triggered-constant primitive) and a `vol_stop(length, k)` +**composition** `k·√EMA(Δ²)` (a composition of `Mul` / `Sqrt` primitives). +**`PositionManagement`** (`aura-std`) is the stateful heart: it latches the +entry-cycle stop distance as the immutable R-denominator, marks against the one-cycle-lagged fill (no look-ahead, C2), and emits a **dense 14-column per-cycle -R-record** (one row per eval, C8; the trade ledger is the `closed_this_cycle` subset, -the R-equity is `cum_realized_r + unrealized_r`, the window-end open trade is the last -`open=true` row). The **`Sizer`** (`size = risk_budget / stop_distance`, flat-1R) is -the feed-forward sizing seam, and **R is size-invariant** — scaling `risk_budget` -leaves every `realized_r` unchanged (pinned by a RED test). **`summarize_r`** is a +R-record** (one row per eval, C8; the trade ledger is the `closed_this_cycle` +subset, the R-equity is `cum_realized_r + unrealized_r`). The **`Sizer`** (`size = +risk_budget / stop_distance`, flat-1R) was the feed-forward sizing seam, and **R is +size-invariant** — scaling `risk_budget` leaves every `realized_r` unchanged +(pinned by a RED test); per the 2026-06-28 reframe the Sizer and `size` / `volume` +are removed from research (size is a deploy concept). **`summarize_r`** is a post-run fold (sibling of `summarize`, **not** an in-graph node) → `RMetrics` (E[R], -SQN, win-rate, profit-factor, max-R-drawdown, conviction terciles, net-of-cost). The -**RiskExecutor** ships as a public `aura-composites` composite-builder -(`risk_executor(StopRule, risk_budget)` — bias+price roles embedding -`stop-rule → Sizer → PositionManagement`, beside `vol_stop`) with a `StopRule{Fixed,Vol}` -**structural axis** (C11); the **Veto** stays a documented seam, not a runtime node (a -pass-through identity is exactly what C19/C23 DCE deletes). The layer is **operable -from the CLI**: `aura run --harness ` — a **compile-time** selector -over Rust-authored harnesses (C9/C17: the CLI *runs*, it does not wire) — folds -`summarize_r` into `RunMetrics.r` (additive; `skip_serializing_if` keeps pip-only and -legacy `runs.jsonl` JSON byte-unchanged), the `stage1-r` harness fanning one bias into -both `SimBroker` (pip) and the RiskExecutor (R) for an honest dual yardstick; an -`r_equity` tap charts the by-trade R-equity through the existing `aura chart --tap`. -The R-record redundancy `debug_assert` uses a **scale-robust relative tolerance** (an -absolute `1e-9` panicked on tiny-pip FX where the `entry − stop` denominator -reconstruction loses precision; the stored `realized_r` is exact). Composites live in -the dedicated `aura-composites` crate (the engine's convenience layer over the standard -nodes — extracted to its own crate after this note), so `aura-engine`'s runtime -dependency stays `aura-core`-only and `aura-std` is now an `aura-engine` -`[dev-dependencies]` — the graph stays acyclic. **Stage 2** (currency P&L, fixed-fractional compounding through the z⁻¹ -fill-edge register, realistic brokers consuming the position-event table) remains the -deferred downstream layer, entered only after `E[R] > 0`. -**Realization (cycle 0066).** SQN is now the operational single-number objective for -ranking a Stage-1 sweep family by signal quality — C12 **axis-2 (argmax-metric)** over -the C18 family store. `metric_cmp` (`aura-registry`) learns the higher-is-better R -metrics `sqn`, `expectancy_r`, `net_expectancy_r`; a member with no `r` block sorts -last (`NEG_INFINITY`), so a pip-only family ranked by an R metric degrades to ordinal -order rather than erroring. `aura sweep --strategy stage1-r` produces the rankable R -family, each member folding `summarize_r` into `RunMetrics.r`. The default grid varies -**only the signal** (`fast`/`slow` SMA lengths), holding the stop and sizing fixed: -`risk_budget` is R-invariant and `bias.scale` is sign-only under flat-1R (both -degenerate axes for the ranked metric), and — load-bearing — the **stop defines 1R**, -so varying it would change what R *means* per member and break cross-member SQN -comparability (the motivation for the deferred n-normalized SQN100, #130). Each swept -member's manifest records the fixed R-defining params (stop, scale) beside the floated -knobs, so a member is reproducible from its own manifest (C18) and the constant-stop -basis of comparability is auditable from the family record. Per-member `--trace` for -the stage1-r sweep is refused explicitly (not a silent no-op) pending #135. +SQN, win-rate, profit-factor, max-R-drawdown, conviction terciles, net-of-cost); +per the 2026-06-28 reframe it folds the cost-model's net-R rather than recomputing +a scalar cost. The **RiskExecutor** ships as a public `aura-composites` +composite-builder (`risk_executor(StopRule, risk_budget)`) with a +`StopRule{Fixed,Vol}` **structural axis** (C20); per the 2026-06-28 reframe its +Sizer interior and `risk_budget` arg are dropped / vestigial, the **Veto** stays a +documented seam, not a runtime node. The layer is operable from the CLI: `aura run +--harness ` — a compile-time selector over Rust-authored +harnesses (C9/C17) — folds `summarize_r` into `RunMetrics.r`, the `stage1-r` +harness fanning one bias into both `SimBroker` (legacy pip) and the RiskExecutor +(R); an `r_equity` tap charts the by-trade R-equity. Composites live in the +dedicated `aura-composites` crate, so `aura-engine`'s runtime dependency stays +`aura-core`-only and `aura-std` is an `aura-engine` `[dev-dependencies]` (the graph +stays acyclic). -**Realization (cycle 0067, #130 + #135).** The two follow-ups the 0066 note flagged -are now shipped. **#130 (SQN100):** `RMetrics` gains `sqn_normalized = -(mean_R/stdev_R)·√(min(n, 100))` — the n-normalized "SQN score" (van Tharp's cap, -`SQN_CAP = 100`), turnover-robust where the raw `sqn` rewards sheer trade count. It is -an **opt-in** rank key (`Metric::SqnNormalized`); the raw `sqn` and the default ranker -stay byte-unchanged, and the field carries `#[serde(default)]` (C18 — a pre-0067 `r:` -block reads back with `sqn_normalized = 0`). Below the cap (`n ≤ 100`) it equals the raw -`sqn` exactly. **#135 (stage1-r `--trace`):** `stage1_r_sweep_family` now persists each -member's equity/exposure/r_equity under `runs/traces///` via the same -`persist_traces_r` the single run uses (mirroring `momentum_sweep_family`); the 0066 -refusal guard is gone, so per-member `--trace` is symmetric across all three sweep -strategies and a swept member charts (`chart / --tap r_equity`). +**Realization (cycle 0066).** SQN is the operational single-number objective for +ranking a Stage-1 sweep family by signal quality — C12 **axis-2 (argmax-metric)** +over the C18 family store. `metric_cmp` (`aura-registry`) learns the +higher-is-better R metrics `sqn`, `expectancy_r`, `net_expectancy_r`; a member +with no `r` block sorts last (`NEG_INFINITY`). `aura sweep --strategy stage1-r` +produces the rankable R family, each member folding `summarize_r` into +`RunMetrics.r`. The default grid varies **only the signal** (`fast` / `slow` SMA +lengths), holding the stop and sizing fixed: `risk_budget` is R-invariant and +`bias.scale` is sign-only under flat-1R, and — load-bearing — the **stop defines +1R**, so varying it would change what R *means* per member and break cross-member +SQN comparability. Each swept member's manifest records the fixed R-defining params +beside the floated knobs (reproducible from its own manifest, C18). -**Realization (cycle 0068, #115 — position-event derive).** The Stage-2 audit layer's -*derivation* now landed (the schema was 0063, #114; the realistic brokers consuming it -remain #116). `derive_position_events(record, instrument_id) -> Vec` -(in `aura-analysis` since cycle 0079, #136; originally `aura-engine`, beside -`summarize_r`) is the **first difference of the executed book**: -a pure post-run reduction over the `PositionManagement` dense record (read positionally -as type-erased `Scalar`s, C7 SoA — no in-graph node, so the hot path stays domain-free, -C14), emitting a `Buy`/`Sell` at each open and a `Close` at each exit, a reversal (or a -stop-then-same-cycle reopen) emitting **Close then the opposite open at one `event_ts`** -(close first — the C8 ">1 event per instant" case that forces this to be a *derived* -table, not a per-`eval` output). The close sizes the **actual book** (the closed -position's stored volume), never an exposure delta — the post-reframe replacement for the -rolled-back 0064 exposure-integral derive (#117). `instrument_id` is a caller-supplied -scalar (`aura-analysis` depends only on `aura-core`, so it never imports an instrument -spec from `aura-ingest`). -A position open at window end emits its open with **no synthetic `Close`** (the table -records actual executed events; `summarize_r`'s force-close is for the R metric only). -The `r_col` ⟷ PM-record lockstep is now guard-pinned for `direction` too. **Still #116:** -fixed-fractional / currency / equity-feedback sizing (the equity→Sizer z⁻¹ fill-edge -register) and the realistic brokers that consume the table. +**Realization (cycle 0067, #130 + #135).** **#130 (SQN100):** `RMetrics` gains +`sqn_normalized = (mean_R/stdev_R)·√(min(n, 100))` — the n-normalized "SQN score" +(`SQN_CAP = 100`), turnover-robust where raw `sqn` rewards trade count. It is an +**opt-in** rank key (`Metric::SqnNormalized`); raw `sqn` and the default ranker +stay byte-unchanged, and the field carries `#[serde(default)]` (C18). Below the cap +(`n ≤ 100`) it equals raw `sqn` exactly. **#135 (stage1-r `--trace`):** +`stage1_r_sweep_family` persists each member's equity / exposure / r_equity under +`runs/traces///` via the same `persist_traces_r` the single run uses; +per-member `--trace` is symmetric across all three sweep strategies. + +**Realization (cycle 0068, #115 — position-event derive).** [The derivation +survives as the **deploy / reconciliation** layer per the 2026-06-28 reframe — its +"realistic brokers consuming it" goal is retired; money is the live-edge concern.] +`derive_position_events(record, instrument_id) -> Vec` (in +`aura-analysis` since cycle 0079, #136; originally `aura-engine`, beside +`summarize_r`) is the **first difference of the executed book**: a pure post-run +reduction over the `PositionManagement` dense record (read positionally as +type-erased `Scalar`s, C7 SoA — no in-graph node, so the hot path stays +domain-free, C14), emitting a `Buy` / `Sell` at each open and a `Close` at each +exit, a reversal (or stop-then-same-cycle reopen) emitting **Close then the +opposite open at one `event_ts`** (close first — the C8 ">1 event per instant" case +that forces a *derived* table, not a per-`eval` output). The close sizes the +**actual book** (the closed position's stored volume), never an exposure delta. +`instrument_id` is a caller-supplied scalar (`aura-analysis` depends only on +`aura-core`). A position open at window end emits its open with **no synthetic +`Close`** (the table records actual executed events; `summarize_r`'s force-close is +for the R metric only). The `r_col` ⟷ PM-record lockstep is guard-pinned for +`direction` too. ### C11 — Generalized sources; record-then-replay determinism boundary **Guarantee.** A source is anything that produces timestamped scalar streams —