design(C10): retire the realistic broker — cost is a cost-model graph in R (gross→net), money to the live deploy edge (refs #116)

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.
This commit is contained in:
2026-06-28 11:51:56 +02:00
parent 226bf57130
commit 29cdc8c218
+340 -225
View File
@@ -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 <sma|macd|stage1-r>` — 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 <sma|macd|stage1-r>` — 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/<n>/<member_key>/` 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 <n>/<member> --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<PositionEvent>`
(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/<n>/<member_key>/` 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<PositionEvent>` (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 —