The single-file ledger had grown to 2968 lines / ~42k tokens, mixing current design law with accreted history: 59 cycle-stamped realization blocks, 18 [HISTORY] passages, 22 supersession markers, and the C10 / C22 / C24 reframe sagas layered several supersessions deep. A code-grounding audit (31 agents, adversarially verified) confirmed 11 defects stated as current truth: stale crate homes from the C28 #288 roster split (cost nodes, PositionManagement, PositionEvent, Session), the renamed InputSpec->PortSpec, the pre-#241 project model in C16 and the open-threads section, a stale HarnessKind retirement deferral in C24, and three C28-internal inconsistencies. New shape, per the ailang precedent: - INDEX.md stays the sole addressable entry point: foundation, external components, a C-id-keyed contract map (one line per contract), and only the genuinely open architectural threads. - contracts/cNN-<slug>.md carries each contract's current truth only: Guarantee / Forbids / Why with ratified refinements integrated, plus a code-anchored Current state. All confirmed defects are fixed here; crate anchors were re-verified against the tree. - contracts/cNN-<slug>.history.md (18 sidecars) and INDEX.history.md preserve every superseded block verbatim, stamps and issue refs intact, under a frozen-record banner. Nothing was deleted: superseded design intent remains an addressable working-tree artifact, off the per-cycle audit walk. - Ledger discipline is now stated in INDEX.md: live files are edited in place at cycle close, superseded text moves verbatim to the sidecar, and a supersession marker in a live file is itself an audit finding. Every contract file was verified against its old text by an independent zero-loss pass (statement-by-statement) plus a code-accuracy spot check; C-ids and contract titles are unchanged, so existing C-id citations in code, tests, and issues resolve as before.
26 KiB
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
most one record per eval), so a strategy emits one signed, bounded bias
f64 ∈ [-1, +1] per cycle (per instrument) — the sign is direction, the
magnitude is conviction, and conviction is optional (a bare ±1 / 0 is the modal
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; 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 (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 position-management either drops
its size input port, 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 — 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).
Cost is a COST MODEL — a composable C9 graph of cost nodes, in R, that
approximates (never claims) realism. The realistic broker is retired: 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-strategy,
the cost-graph composite-builder (cost_graph) in aura-composites (C16), 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 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.
Co-temporality contract (load-bearing, generalizes to all cost factors).
Because summarize_r positionally joins cost[i] ↔ record[i], a cost node is
gated only by the position-management trade-geometry; any not-yet-warm state
input (a vol proxy warms later than the executor) contributes 0 cost that
cycle rather than withholding — the node still emits its row, so the cost stream
stays co-temporal 1:1 with the executor's R-record. Co-temporality is therefore
structural and warm-up-independent, preserves the C18 golden, and is honest (no
slippage estimate yet → no charge). A constant-per-trade factor satisfies it
trivially; only state-dependent factors need the missing-factor → 0 rule.
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 there is no z⁻¹ fill-edge register and no
flat-1R-vs-compounding structural axis in the research loop (with no in-loop
feedback there is nothing for a register to cut; 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, distinct
from the removed money-Sizer; it is tested via the conviction_at_entry record
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 reliable (belastbare) 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 legacy pip-equity, unsized-exposure node) is redundant as a
quality measure with the net-R cost model in place (R displaces pips). It is retained as a legacy / simple optional pip yardstick
(still wired alongside the R executor in the r-family member for an honest dual
readout), not part of the new model and not to be expanded.
Forbids. Putting sizing or the stop in the strategy (bias is unsized; the
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 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
reliable 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. Industry grounding for the bias/R spine: LEAN /
nautilus_trader / backtrader / QSTrader / vectorbt / zipline (#117 decision log).
Current state
Bias and executor. The unsized strategy output is the Bias node
(aura-strategy/src/bias.rs). The per-symbol RiskExecutor is the
risk_executor(StopRule, risk_budget) composite-builder
(aura-composites/src/lib.rs) with a StopRule{Fixed, Vol, VolTf} structural
axis (C20); risk_executor_vol_open is the gridding sibling with the two vol-stop
knobs left open as sweep axes. Stop rules: FixedStop (a triggered constant) and
the vol_stop(length, k) composition k·√EMA(Δ²) built from Mul/Sqrt
primitives; VolTfStop for a resampled-timeframe vol stop (FixedStop in
aura-strategy/src/stop_rule.rs, vol_stop composite in aura-composites). The
Veto is a documented seam, not a runtime node.
The Sizer is a vestige, not yet removed. The ratified design (#116) has no
Sizer in research, but the composite still physically wires
stop → Sizer → PositionManagement, with the Sizer bound to a constant
risk_budget (risk_budget is a formal argument; the runner calls
risk_executor(stop, 1.0) — aura-runner/src/member.rs). This is the
constant-unit-driven-size-port resolution of C8 wiring totality (not the
drop-the-port resolution): risk_budget is a constant, never equity-fed, so no
equity → size edge exists and the pure-feed-forward invariant holds; under R
size-invariance the Sizer's size = 1.0 / stop_distance carries no research
information. Physically deleting the Sizer node and its risk_budget argument is
outstanding cleanup toward the ratified shape.
Position management and R metrics. PositionManagement
(aura-backtest/src/position_management.rs) 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 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). summarize_r
(aura-backtest/src/metrics.rs) is a post-run fold (sibling of summarize, not
an in-graph node) → RMetrics (E[R], SQN, sqn_normalized, win-rate,
profit-factor, max-R-drawdown, conviction_terciles_r, net_expectancy_r). It
folds a co-temporal cost stream (positional 1:1 join) into net_expectancy_r —
one home for cost, byte-identical to the cost = 0 baseline on an empty stream.
sqn_normalized = (mean_R / stdev_R)·√(min(n, 100)) (SQN_CAP = 100) is an opt-in
turnover-robust rank key; below the cap it equals raw sqn exactly. SQN is the
operational single-number ranking objective; a sweep's default grid varies only
the signal, holding the stop fixed — the stop defines 1R, so varying it
across members would change what R means per member and break cross-member SQN
comparability.
Cost model. aura-strategy/src/cost.rs owns the abstraction: the 3-field cost
triple {cost_in_r, cum_cost_in_r, open_cost_in_r} is one source of truth
(COST_FIELD_NAMES / COST_WIDTH = 3, mirroring the position-management record),
prefixed by the 4-wide GEOMETRY_WIDTH geometry inputs. The CostNode factor
trait carries a node's only per-node difference — the price-unit cost numerator
(cost_numerator), plus extra_inputs, label, and a defaulted charge_mode();
everything else is the generic CostRunner<F: CostNode> adapter (a plain
downstream Node, C9; no runtime sub-object), which writes the co-temporality
skeleton (geometry-only gating, numerator / latched R-normalization, the
closed/open charge, the running cum, the 3-field emit) once. ChargeMode {AtClose, PerHeldCycle} selects timing per factor (a commission is intrinsically
at-close, a carry per-held-cycle); the single CostRunner handles both arms, the
PerHeldCycle arm accruing per into a per-position acc each held cycle and
dumping it into cum at close. Three shipped factors, all aura-strategy:
ConstantCost (a labelled stress-parameter, cost_per_trade / |entry − stop|),
VolSlippageCost (slip_vol_mult · vol / |entry − stop|, reading an independent
short-horizon realized-range vol proxy distinct from the stop's own vol), and
CarryCost (a ConstantCost twin with charge_mode() = PerHeldCycle, the flat
base of the accrual family). CostSum (aura-strategy/src/cost_sum.rs) is the
cost-graph output node, summing N nodes' 3-field records per-field into one
aggregate (n = 1 is the identity), so the seam consumes a single cost stream
regardless of node count. cost_port / intern_port (aura-strategy/src/cost.rs)
intern runtime port names process-globally (the COL_PORTS production pattern,
#152), reused across per-member rebuilds. cost_graph(Vec<PrimitiveBuilder>)
(aura-composites/src/lib.rs) is the authoring primitive: it fans the 4 geometry
inputs to N cost nodes, surfaces each node's extras
(schema().inputs[GEOMETRY_WIDTH..]) as cost[k].<port> roles, sums through
CostSum, and exposes the 3-field aggregate at arbitrary arity. The headline sink
is net_r_equity = LinComb(4)[cum_realized_r, unrealized_r, −cum_cost_in_r, −open_cost_in_r] → Recorder (C8/C18), a sibling of r_equity, emitted only when a
cost is authored; a per-held-cycle factor bleeds continuously over the hold because
the bleed lives in open_cost_in_r, which this tap already subtracts (Approach B,
no summarize_r fold change).
Cost on the campaign / sweep path (net is the default). Cost is authored as
the campaign document's additive cost: Vec<CostSpec> block
(aura-research/src/lib.rs, CampaignDoc) — a closed, externally-tagged
vocabulary over the three shipped nodes (constant / vol_slippage / carry,
field names = the builders' ParamSpec names), mirroring the risk block (serde
default + skip-if-empty: cost-less docs hash byte-identically, C18). An absent
block is the explicit zero-cost model and summarize_r's net family equals gross
under it — every result is net, no second gross-labelled result kind.
cost_nodes_for (beside stop_rule_for_regime, aura-runner/src/translate.rs) is
the one doc→builder binding; wrap_r (aura-runner/src/member.rs) carries the
optional cost leg (cost_graph off the executor's four geometry outputs, the vol
proxy in production, a gated cost recorder in reduce mode as the summarize_r join
input, the LinComb(4) net_r_equity curve in trace mode). Both re-run sides
re-derive the model (cost_specs_from_params), so the C1 drift alarm compares like
with like; costed families reproduce bit-identically (incl. Carry).
TapChannel::Net (aura-runner/src/runner.rs) routes net_r_equity to persisted
curves; a cost-less doc requesting it keeps a remedy-naming skip notice. There are
no --cost-* run-path flags (removed #221/#234); cost travels in the document.
Risk regime as a structural campaign axis. The StopRule{Fixed, Vol} axis is
realized at the campaign-document level as CampaignDoc.risk: [RiskRegime]
(aura-research, variants Vol{length, k} and VolTf{period_minutes, length, k}
(#262), the fixed-stop rule additive when needed) — a kept-separate matrix axis,
peer of instruments and windows. The executor keys the nominee map by (strategy, window, regime), so generalize aggregates within a regime, never across.
Regimes are compared at presentation, never argmax-selected across (a
cross-regime E[R] argmax would compare R-multiples in different R units). Each
member manifest stamps its resolved stop (default included). Two default
representations coexist by design (#217): a dissolved sweep binds no regime
(risk: [], late-resolved per member by stop_rule_for_regime), while
walkforward/mc/generalize bind the default regime eagerly
(risk: [Vol{length:3, k:2.0}]) — same R behaviour, deliberately different
document content-ids. Deferred: regime-aware trace persistence — the trace
re-run and cell-key dir naming still assume the default stop, since
CellRealization carries no regime (#212).
Legacy pip yardstick. SimBroker (aura-backtest/src/sim_broker.rs, node kind
simbroker) is a legacy pip-equity node, retained as an optional pip yardstick
and still wired alongside the R executor in the r-family member for an honest dual
pip/R readout; not part of the cost model, not to be expanded.
Position-event table (deploy / reconciliation). 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) and
derive_position_events(record, instrument_id) -> Vec<PositionEvent> live in
aura-backtest/src/metrics.rs as post-run value types (not per-eval nodes, C8).
action serde-encodes as a bare i64 (Buy=0, Sell=1, Close=2), the C7 scalar
column form, out-of-range rejected on read. derive_position_events is the first
difference of the executed book — a pure reduction over the position-management
dense record (read positionally as type-erased Scalars, 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) emits Close
then the opposite open at one event_ts (the C8 ">1 event per instant" case that
forces a derived table). The close sizes the actual book (the closed
position's stored volume), never an exposure delta. 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 table stays
broker-independent.
CLI. The run/sweep surface is blueprint-driven — aura <verb> <blueprint.json>
over examples/r_*.json / crates/aura-cli/tests/fixtures/; the built-in
--harness / --strategy selectors were retired with the demos → blueprint-data
cut (#159). The strategy identifier family is the r-family — r-sma /
r-breakout / r-meanrev (renamed from the Stage-1 family in #174). metric_cmp
(aura-registry) ranks the higher-is-better R metrics sqn, expectancy_r,
net_expectancy_r, sqn_normalized; a member with no r block sorts last.
Deferred work. Physical removal of the vestigial Sizer and its risk_budget
argument (ratified shape, #116). Data-grounded cost factors beyond the shipped
stress parameters: a notional-based carry (price × rate, reads the price tap) and
the calendar-aware overnight swap proper (rollover-boundary timing, 3× Wednesday,
long/short asymmetry — deploy-edge realism) (#148). The conviction-weighting
R-aggregation axis (flat vs |bias|·realized_r), tested via conviction_at_entry /
conviction_terciles_r (#148). Regime-aware trace persistence (#212). Money, a
real broker, and cTrader Open API remain the separate live / deploy-edge concern
(C11 record-then-replay, C13 frozen deploy).
See also
- C1 — determinism / bit-identity (costed families reproduce byte-for-byte)
- C2 — no look-ahead (one-cycle-lagged fill; the co-temporality 0-cost warm-up rule)
- C8 — ≤1 record per
eval; sinks are the only displayable surface - C9 — cost nodes are ordinary downstream nodes; the cost model is a C9 graph
- C11 — record-then-replay; the live broker as recorded I/O; bit-identity of re-runs
- C13 — the frozen-deploy invariant (deploy = frozen bot + broker connection)
- C14 — the domain-free engine (no cost / broker logic in
aura-engine) - C15 — the
instrument_geometrysidecar (pip value / stop-distance-in-currency) - C16 — the composites layer (
cost_graph,risk_executor) - C18 — the registry / golden (byte-identity, content-addressed docs)
- C20 — structural axes (
StopRule, the risk regime) - C23 — names non-load-bearing; DCE of the absent Veto
- C24 / C25 — experiment intent in the campaign document
- C28 — the crate-layer ladder
History: c10-bias-r-cost.history.md