Ledger + glossary: C10 R-reframe (bias / R / RiskExecutor / Stage-1·2) #117
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Record the C10 A-side realization in the design ledger (docs/design/INDEX.md, under C10's realization notes) and the glossary (docs/glossary.md). The load-bearing part is the Architecture-A clarification of a real over-specification in C10 as originally written:
event_tsSource replay (which does not exist and carries a reversal determinism hole).slippage.Glossary: revise
realistic broker(single-pass node on exposure) andposition-event table(decoupled audit view); addcurrency equity,net_currency_pnl,n_trades.depends on: the implementation issues (done at cycle close / audit)
context: a contract reinterpretation belongs in the ledger, not silently in code.
Reframe — this issue's original scope is superseded and repurposed (boss run, 2026-06-23).
The Architecture-A clarification in the body above is no longer the C10 direction. Following an in-context design review — industry-grounded against LEAN, nautilus_trader, backtrader, QSTrader, vectorbt and zipline — the user ratified a deeper reframe of C10. This issue now tracks the C10 R-reframe amendment (design ledger +
CLAUDE.mddomain-invariant #7 + glossary), and serves as this run's reference issue / decision log.Run anchor: main HEAD
2e0500d(session anchor for the boss rollback sandbox).Fork decisions (decision log)
R adopted as the native unit. The strategy answers "how much R out per 1R risked?"; R is defined by the stop. R replaces pips as the account/instrument-agnostic signal-quality yardstick. Rationale: pips are not risk-normalized, R is. Grounded: R-multiple / expectancy is a recognized standard pattern (Van Tharp), decoupled from currency P&L.
exposure → bias. The strategy's primary output is renamed and re-semantized from a signed fractional position
f64 ∈ [-1,+1](which conflated direction + conviction + size) to a bias = direction + conviction only, explicitly unsized. Sizing leaves the strategy. Grounded: LEAN Insight, backtrader Sizer, QSTrader/zipline all separate an unsized directional signal from downstream sizing (4 of 5; nautilus fuses them — so not unanimous, and we sit on the LEAN side, the right one for signal-quality research).Risk-based execution layer = a RiskExecutor composite.
bias → stop-rule → Sizer (in R) → Veto/gate → position-management, per symbol; nested inside a Broker/Account composite that aggregates per-symbol P&L → account equity. Naming: the sizing node is the Sizer, NOT a "risk-manager" — in LEAN/nautilus "risk management/engine" denotes a pre-trade veto layer, which we keep as a distinct Veto seam. The size / veto / fill roles stay distinct inside the composite.Stage-1 / Stage-2 cleave. Stage 1 (R / signal quality): flat-1R sizing → feed-forward, no equity feedback → R-expectancy; the primary research loop. Stage 2 (currency / deploy viability): fixed-fractional sizing reads equity (compounding) → realistic brokers, real frictions → currency equity. The cleave itself is our own synthesis (closest precedent: vectorbt's vectorized-signal vs sequential-money-management split) — owned, not claimed as industry-standard.
The only feedback is the equity→Sizer cycle, cut by a
z⁻¹register on the FILL edge (mark-to-market stays a same-cycle price read, C2). Encapsulated inside the executor composite. Flat-1R DCEs the register away (Stage-1 feed-forward); compounding keeps it live (Stage-2). Flat-1R vs compounding is a structural axis (C11: instantiating the feedback edge is topology, not a sweep param). The explicit register is required because C11 reorders the flat graph — the implicit read-before-fill ordering by which every surveyed framework cuts the loop would not survive CSE/DCE.Position-event table stays the decoupled, derived, Stage-2 audit layer — now the first difference of the book via book-tracking (
deal = target − book − in_flight), never a per-evaloutput (C8). The flawed 0064 exposure-integral derive is abandoned.Account mode (netting/hedging) = composition constraint. Hedging (EU CFD default): many RiskExecutors per symbol, even opposite. Netting: ≤1 per (strategy, symbol); multi-symbol strategies work on netting.
Milestone reconciliation (ratified)
The existing "Realistic broker & position-event table (C10 A-side)" milestone is resequenced as Stage 2, not discarded. A new Stage-1 "R-based signal quality" milestone is inserted before it. #113 / #114 (foundation, committed locally, unpushed) survive for Stage 2; #115 (rolled-back 0064) and #116 to be rewritten to the new taxonomy; #119 (stops) promoted to foundational (the stop defines R). Tracked in follow-up tracker edits this run.
Ledger + glossary: C10 Architecture-A clarification (single-pass broker, table = audit view)to Ledger + glossary: C10 R-reframe (bias / R / RiskExecutor / Stage-1·2)Stage-1 kickoff handoff (fresh-session start point).
The C10 R-reframe is recorded (ledger C10 +
CLAUDE.md#7 + glossary, commitf040b66) and the tracker is restructured. A fresh session resumes the Stage-1 work here.Queue: milestone "R-based signal quality (Stage 1)" —
Contract: ledger C10. Stage 2 (realistic broker, currency) is the existing milestone, sequenced after, entered only once
E[R] > 0.Suggested first cycle: a
brainstormof the Stage-1 node interfaces — the open design questions the reframe deliberately left to node-level design:Exposure → Biasrename is behaviour-preserving (compiler-driven-eligible) but best landed at the head of this cycle, coordinated with the bias node's consumer (the Sizer).State: main HEAD
f040b66, working tree clean, nothing pushed. The prior run's session anchor was2e0500d; the reframe decision log is the comment above.Stage-1 node-interface decisions — locked autonomously (boss), adversarially hardened.
The Stage-1 "R-based signal quality" node interfaces are decided. A 12-juror
adversarial refute-panel ran over the candidate decisions before locking; all 12
returned NEEDS_REFINEMENT (none sound-as-first-drafted, none fatal), and the
refinements below are folded in. The panel earned its keep — three of these would
have shipped a measurement defect.
The locked design (per fork)
Stop-rule emits a DISTANCE, latched at entry as the frozen R-unit. The stop-rule
node is a pure per-cycle
stop_distance: f64(price units, direction-agnostic).position-management latches the entry-cycle distance ONCE as the immutable
R-denominator (
R = (exit−entry)·dir / latched_distance) and tracks the stop LEVELseparately. Fixed level for the keystone; trailing (level moves monotonically toward
the position off the live distance) is a designed-for structural axis (C20), not built
yet. Never re-latch the R-unit mid-trade — that was the D1/D2 look-ahead trap.
First concrete stop = a volatility stop, not a constant. A constant distance makes
R = pip_pnl/d, i.e. R-expectancy is just pip-expectancy rescaled — the risknormalisation that distinguishes R from pips vanishes. So the keystone ships
VolStop=
k · EMA_length(|price − z⁻¹ price|)as ONE fused node (noAbs/Mulprimitiveexists, so the fusion is mandatory anyway), Kahan-safe like
Sma,Noneuntil warm.The volatility that sets 1R is sampled at entry and frozen for the trade (C2-safe,
the analogue of SimBroker's
prev_exposurefreeze). A trivialFixedStop(distance)ships alongside as a test fixture and the fixed-vs-vol structural-axis sibling. True
ATR (true-range) is a later overlay — it needs OHLC, unwired today.
Stop-outs are NOT capped at exactly −1R. The single most important correction.
Capping every adverse exit at −1R truncates the loss tail and biases E[R] upward,
precisely on breakout/trend strategies (the repo's only example class). Instead:
realise
R = (fill − entry)·dir / latched_distancewhere the stop fill is no betterthan the stop level (gap-through → R < −1, the honest tail). With close-only data the
stop is tested at the close with a documented optimistic bias (no intrabar
excursion visible); when a resampled high/low is present, test against the adverse
extreme (low for long / high for short) and fill no better than the stop. Frictionless
fill at the stop yields exactly −1R only in the no-gap case.
Fills are one-cycle-lagged (no look-ahead). position-management latches
entry_priceand marks every exit (stop / bias-flip / target) against the fill held into the
cycle (SimBroker's "update state AFTER taking the outcome" discipline), never the
same-cycle price the stop breach is tested against. Encoded as intra-node register
state (survives the C23 flat-graph reorder; it is NOT a graph cycle, so it needs no
back-edge support). A RED no-look-ahead test is mandatory from day one, parallel to
sim_broker_no_lookahead: a bias-flip-exit trade's R must equal the lagged-fill R, anda stopped trade is exactly −1R independent of fill in the no-gap case.
The per-trade R-outcome is a sparse multi-column record (C8-safe). position-management
returns
Some(record)only on a close cycle,Noneotherwise — a single net positioncloses ≤1 trade per cycle (a reversal closes one leg and reopens; the reopen is not an
outcome), the genuine asymmetry vs. the position-event table (which needs >1/instant).
Record columns:
r_multiple: f64(authoritative, debug-asserted againstdir·(exit−entry)/|entry−stop|),exit_reason: i64(enum: stop / bias-flip /reversal-leg / window-end),
was_stopped: bool(the one attribution prices can'tlosslessly reconstruct under gaps),
entry_ts/exit_ts: timestamp,entry_price/stop_price/exit_price: f64,direction: i64 ∈ {−1,+1},bias_at_entry_abs: f64(conviction, for the calibration diagnostic),size: f64.The R-evaluator is a POST-RUN FOLD (
summarize_r), not an in-graph node. Recordertaps are drained post-run (
rx.try_iter().collect(); the playground is web-from-disk,C22 amendment) — nothing consumes a recorder stream mid-run, so an in-graph R-curve node
buys nothing and double-maintains state with a drift risk and a broken (sparse, gapped)
time axis. Stage 1: position-management emits the sparse R-outcomes → a
Recordertapsthem →
summarize_r(sibling ofsummarize) folds the trade ledger into the metricsand a by-trade cumulative-R curve (running partial-sums, labelled by-trade, not
time-drawdown). The in-graph running-equity node is held in reserve for Stage 2, where
compounding genuinely needs it through the z⁻¹ fill-edge register.
The Sizer is a load-bearing seam (
size = risk_budget / stop_distance, flat-1R)."size = 1.0 constant" is constant-notional, not flat-1R; true flat-1R is risk-per-trade
constant ⇒ size varies inversely with the stop. The Stage-1 body emits
size = 1 / stop_distance(risk_budget = 1R), so it actually reads itsstop_distanceinput and theinterface is exercised — Stage 2 swaps the constant for
risk_fraction · equity, sameshape. R is computed size-invariantly by position-management, so size never
contaminates the R-multiple (the property that makes Stage 1 feed-forward). Two tests:
the flat-1R invariant (
size·stop_distance ≡ risk_budget) and R-invariance under sizescaling.
The Veto is OMITTED as a runtime node in Stage 1. A pass-through identity node is
exactly what C19/C23 DCE is licensed to delete; pinning it to survive would be an
implicit-eval-order dependency C23 forbids. It is reserved as a documented seam (a
named position in the spec, not a
BlueprintNode), added as a real gating node with areal edge the cycle it first vetoes.
Conviction: sign-only sizing, but magnitude is measured. flat-1R +
sign(bias)entry,equal-weighted E[R] as the headline (do NOT conviction-weight the fold — that leaks
sizing into the yardstick). But
|bias_at_entry|is recorded per trade andsummarize_rreports a conviction-calibration diagnostic (per-tercile E[R] / rank-corr(|bias|,R)),
so a strategy whose edge is conviction-ranking (E[R]≈0 equal-weighted, strong
corr(|bias|,R)) is not mis-reported as worthless. The deferred
|bias|>θthreshold fallsout as the 2-bucket read.
The RiskExecutor is a per-symbol Composite, bias+price as input roles (price fanned to
stop-rule and position-management —
GraphBuilder::feedsupports fan-out), exposing theR-outcome;
summarize_r(the R-evaluator) sits downstream/outside, folding the recordedoutcomes (matches C10 verbatim and the SimBroker-emits / summarize-folds pattern). The
Broker/Account composite (nesting + equity aggregation) is Stage 2, not built now.
Metric set (all new
RunMetricsfields#[serde(default)]forruns.jsonlback-compat): E[R], win_rate, n_trades, avg_win_R, avg_loss_R, profit_factor, max
by-trade R-drawdown, SQN (
√n · meanR / stdevR, the dispersion-adjusted single-numbersweep objective), the conviction diagnostic, and a net-of-cost E[R] (one round-trip
spread, a price-unit param — still account-agnostic — so a churny strategy's R can't look
fine while costs would sink it). Open trades at window end are force-closed mark-to-close
with
exit_reason = window-endand their count surfaced — never silently folded asunrealized MtM (the bug
summarizehas today).Note on substrate
This is NEW discrete-trade machinery, not a SimBroker extension: SimBroker integrates a
continuous exposure·return into a pip curve (no trade, no stop, no R) — it is the
ancestor in spirit only. Stage 1 adds a discrete close-detector + stop + entry/exit
pairing producing realised per-trade R.
Iteration cut
exposure → biasrename (#126) — behaviour-preserving,compiler-driven, its own commit.
VolStop+FixedStop(#119) + position-management lifecycle producing thesize-invariant sparse R-outcome + the entry-latch / lagged-fill discipline + a core
summarize_rfold (#127 core, #129 core). A complete, runnable, testable R-producingvertical slice (
source → bias → stop-rule → position-management → Recorder,folded by
summarize_r); R-invariance is the load-bearing property pinned here.documented-not-built) + the enriched
summarize_r(SQN, conviction diagnostic,net-of-cost, window-end handling) + the new
RunMetricsfields + the CLI/recordingsurface (#129).
Proceeding to
specifywith this as the ratified design.Refinement (spec-time, 0065): decision #5 (per-trade R-outcome) is sharpened from a sparse Option stream (Some-on-close / None) to a dense per-cycle record (one record/eval, always Some, like SimBroker) carrying a
closed_this_cycleflag. Reason: a sparse stream cannot express the R-equity curve / max-drawdown-in-R (no intra-trade mark) and cannot reveal a position still OPEN at window end (juror D3, strongest point). The dense record subsumes both: the trade ledger is the closed-flagged rows; R-equity = cum_realized_r + unrealized_r; the window-end open trade is the last row (explicit, flagged, counted forced-close — no silent MtM). C8 still holds (one record/eval) and the ≤1-close/cycle asymmetry vs the position-event table is intact. Multi-field output is on a tested path (the Resample out:[Cell;4] precedent; #47 is only the CLI re-export rendering, orthogonal). Full design in spec 0065.Iteration-2 fork decisions (planner, derived — boss). Iter-1 committed
2c43296(green).round_trip_cost: f64param (price units); the 6 callers pass 0.0 for gross. net_expectancy_r = mean over trades of (realized_r - round_trip_cost/|entry_price-stop_price|): a fixed price-spread costs MORE R when the stop is tighter (correct churn-honesty, H1). latched_dist is recovered per closed row from record cols 6/7; r_col gains ENTRY_PRICE=6, STOP_PRICE=7, BIAS_AT_ENTRY_ABS=9.Iter-2 scope = Sizer + RiskExecutor(fixture) + summarize_r enrichment + RunMetrics.r. CLI/recording surface = iter-3.
Correction (2026-06-24): VolStop was a design error — a fused node justified by "no Abs/Mul primitive exists". Per the user, that reasoning is inverted: a node is a PRIMITIVE only if it is NOT DAG-expressible from other primitives; if a function needs a missing primitive, ADD the primitive. VolStop is pure feed-forward arithmetic -> a COMPOSITION, not a primitive.
Decisions:
This lands as a corrective mini-iteration BEFORE iter-2 (Sizer/RiskExecutor), since iter-2 builds on the stop-rule. iter-1 commit
2c43296stop_rule.rs is revised; the iter-1 E2E used FixedStop, so it is unaffected.Stage-1 cycle 0065 — iteration 2 landed (node + metric layer), iteration 3 (CLI surface) is a checkpoint
Committed (autonomous, unpushed, this
/bossrun; session anchor0998f9a):6e21495plan 0067 (iter-2: Sizer + RiskExecutor + metric enrichment)b4e8433feat: the implementation, full suite 500 passed / 0 failed, clippy clean.What shipped (the substantive Stage-1 core):
PositionManagementgains a 4thsizeinput (col 10, from the Sizer); R stayssize-INVARIANT — pinned by a RED test at the node and again end-to-end.
Sizer(aura-std):size = risk_budget / stop_distance, flat-1R; the Stage-2fixed-fractional sizer slots in unchanged.
summarize_renrichment: SQN, conviction terciles, net-of-cost (round_trip_costparam, charged in R via the producer-recovered
latched_dist). Net-of-cost istested through the real producer→consumer seam, not only hand-built rows.
RunMetrics.r: Option<RMetrics>withserde(default, skip_serializing_if)—legacy
runs.jsonldeserialise toNone, pip-only on-disk shape byte-unchanged.RiskExecutorcomposite (aura-engine integration-test fixture):FixedStop → Sizer → PositionManagementbehind openbias+priceroles, price fanned toboth; bias produced in-graph from the single price source (a second bias source
would k-way-merge into separate cycles and mark stale prices). Veto = documented
seam, not a node.
Held quality finding (verified + fixed, not auto-signed past): the executor
fixture read the dense
sizecolumn (10) under a "lockstep" comment, but thecross-crate layout guard pinned only cols 1/6/7/9 — col 10 was unguarded. Brought
col 10 under the guard (
r_col_indices_match_producer_field_layout), so thesize-invariance read is drift-protected like the others.
Decision — iteration 3 (the CLI/recording surface, #129) is a checkpoint, not
autonomous continuation. The node + metric layer is design-settled by spec 0065;
the CLI surface is not. Today every CLI command folds pip metrics via
summarizeand neversummarize_r; wiring R in is design-open and high-context(the CLI is ~2000 lines). Genuine forks it raises:
("shippable home deferred to iter-3"); where it lives as a real API
(aura-engine composite helper? aura-std?) is undecided.
SimBroker (pip). An R run needs bias → stop-rule → RiskExecutor → dense record →
summarize_r. New harness shape + which stop/strategy.RunMetrics.ron every run (the Optionwas designed for this), or a dedicated mode/command? Product-shape, partly user
preference.
This is the natural milestone-chunk boundary (substantive core done & green) and a
high-context next iteration whose session shape is the user's call — so it bounces
to the user rather than continuing autonomously.
iteration 3 — the Stage-1 R CLI surface: fork decisions (boss-derived, user-delegated)
Context. User re-entered
/boss(2026-06-24): "leg los. Du entscheidest das Design. Aura soll von der CLI aus bedienbar sein, vor allem vor dir selbst." Design authority for the iter-3 forks (enumerated in the comment above) is explicitly delegated to the orchestrator; the directive is the product-shape input that settles fork 3 — the CLI must be ergonomic to drive from a shell, especially by Claude itself.Entry-path verdict. iter-3 continues the open cycle 0065 (boss-signed spec
docs/specs/0065-stage1-r-signal-quality.md); it is not a new-cycle bounce. Every load-bearing fork is derivable (contracts C9/C17/C22 + the existing CLI code + the directive), so this isspecifydirect-entry — under/bossthe autonomous signature is the grounding-checkPASS.Scope line drawn (#109 (a)/(b) fork, derived). "Operable from the CLI" = run + inspect a Rust-authored harness from the shell — the (a) reading that C9/C17/C22 already settle — not wiring/authoring topology in the CLI (the (b) reading, which would amend C17/C9 and needs its own brainstorm). The broad #109 meta-level (World, project-as-crate, composable orchestration) stays a separate future milestone; iter-3 is the narrow Stage-1-R single-run surface.
Fork decisions (one substantive sentence each):
RiskExecutor's shippable home → an
aura-enginecomposite-builder (pub fn risk_executor(...) -> Composite), promoted from the iter-2 test fixture: a composite needsGraphBuilder, which lives inaura-engineand notaura-std— the same placement already fixed for thevol_stopcomposite.R sample harness shape → SMA-cross → Bias → RiskExecutor(stop → Sizer → PositionManagement) → R-record tap, reusing the existing pip sample's signal generator so the R harness differs from the shipped pip sample by exactly the risk layer under test, with
vol_stopthe default stop because the milestone's value is a volatility-defined (not pip-rescaled) R.R-scoring surface → additive on
aura run: the run path foldssummarize_rintoRunMetrics.r(theOptionwas designed for exactly this) andRunReportprints the R block when a dense R-record is present — one shell call, one readable result, the directive's "especially for Claude" ergonomics, with no separate mode to remember.Harness selection →
aura run --harness <name>compile-time enumerated selector (sma|macd|stage1-r; the existing--macdkept as a back-compat alias): a fixedmatchover Rust-authored built-ins is pure running — C9/C17-clean, no runtime node registry, no DSL — and generalizes today's ad-hoc--macdflag into one uniform, extensible run verb.R-record charting → a minimal R-equity tap now, rich markers deferred: tapping the dense record's R-equity column (
cum_realized_r + unrealized_r) reuses the existing trace-store +aura chart --taprendering unchanged, so a usable R-curve costs one extra tap rather than a new viewer.Deferred to follow-ons (to be filed as ideas):
runs family rank sqn/rank expectancy_r(SQN as the documented sweep objective) — needs an R sweep-family builder, a larger surface than this single-run cut.iter-3 spec signed (boss auto-sign) + the patch sub-decisions
Amended
docs/specs/0065-stage1-r-signal-quality.mdwith the "Iteration 3 — the CLI surface" section and signed it under/boss(commit08d09df, subject carries(boss-signed)). The autonomous signature is the grounding-check PASS — all substrate assumptions and the post-patch current-code claims ratified by named green tests.An advisory 5-lens spec-skeptic panel ran alongside the gate: criterion / invariants / scope SOUND; ambiguity + plan-readiness returned BLOCK with real findings, resolved inline before signing (then re-grounded — the patch added new current-code claims, all ratified). The sub-decisions those findings forced, one sentence each:
parse_run_argstokenizer: orthogonal--harness×--real×--tracecomposition is impossible under today's exhaustive literal-slice match, so therunarms become a tokenizer (same shape as the existingparse_real_args),--macd→ alias,--harness smathe default.round_trip_cost = 0.0: Stage-1 R is frictionless signal quality (costs are the Stage-2 broker concern), so the run path folds with zero cost andnet == gross; a--costknob is a follow-on.persist_tracesgains a third tap: the helper is 2-tap-hardwired, so ther_equitytap costs a signature/plumbing change (the chart viewer is genuinely unchanged;reduce_for_tap's MinMax default suits the curve).summarize(equity, exposure)reads the SimBroker + Bias taps exactly as today; the RiskExecutor dense R-record is a third independent tap forsummarize_r.exposure_sign_flipsstays: the iter-0 rename tobias_sign_flipswas never carried out (the field is stillexposure_sign_flips), so the example uses the as-built key; the rename is a deferred follow-on.Reply vetoes; otherwise the plan proceeds.
Rename iteration (cycle 0065 tail, #126) — scope fork decided
User green-lit completing the
exposure → biasrename tail. The load-bearing scope fork: whichexposureoccurrences are bias-residue to rename vs SimBroker's genuine exposure-integral concept to keep.Decided (one sentence): rename only the bias-residue; keep SimBroker's pre-reframe exposure concept and the on-disk
exposuretap label.RunMetrics.exposure_sign_flips→bias_sign_flipswith#[serde(alias = "exposure_sign_flips")](the typed serde field the audit flagged as aging worst) + the parallelMcAggregate/MetricStatsfield + the registryMetric::ExposureSignFlipsrank vocab (accept BOTH rank strings for CLI back-compat, rename the variant, update the error/listing); the manifest param labelexposure_scale→bias_scale; the.named("exposure")Bias instances →.named("bias").SimBroker's slot-0exposure+prev_exposure+ itsexposure · price-returnintegral — the pre-reframe pip-quality node the 0065 spec retains unchanged (its input is conceptually an exposure it holds, fed by a bias); and the on-disk / user-facingexposuretap label (the recorded bias series feedingchart --tap exposure+ the byte-pinned trace tests) — renaming it would break a stable chart selector + trace-file name for cosmetic gain.Back-compat is the care-point: new output serializes
bias_sign_flips/bias_scale; legacyruns.jsonlreads via the serde alias (the existing legacy-deserialize tests keep the old key, pinning the alias). RED-first. After this lands, #126 closes and the Stage-1 milestone container is finalized.Delivered: the C10 R-reframe (bias / R / RiskExecutor / Stage-1 vs Stage-2) is in the ledger (docs/design/INDEX.md C10, reframe note 2026-06-23) and the glossary carries every reframe term (bias, R-evaluator, RiskExecutor, Sizer, Stage 1/Stage 2, position-event table, realistic broker, R-metrics). This was the kickoff/decision-log issue for the reframe; its ledger+glossary deliverable is in the tree. Closing as done.