Support per-instrument cost factors in a campaign #260
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?
ConstantCost.cost_per_tradeis a price-unit numerator, R-normalized ascost / |entry − stop|(crates/aura-std/src/constant_cost.rs), so one constant cannot be right across instruments of different price scales (GER40 ~2e4 vs EURUSD ~1.1). The campaigncostlist applies to every cell, which makes any multi-instrument campaign with a constant cost unit-inconsistent.Consequences observed in practice: realistic-spread stress testing forces N per-instrument campaign copies that differ only in one number, and "generalize under constant costs" is inexpressible —
std::generalizeneeds ≥ 2 instruments, which the per-instrument split removes.A per-instrument binding (an instrument-keyed map in the cost entry, or an instrument-relative unit such as pips resolved via the recorded sidecar geometry) would restore the one-campaign form.
Design reconciliation (specify)
Spec: per-instrument-cost. The issue leaves two shapes open ("an instrument-keyed map in the cost entry, or an instrument-relative unit such as pips resolved via the recorded sidecar geometry"); resolutions below are derived orchestrator decisions (recorded 2026-07-13).
"cost_per_trade": { "GER40": 2.0, "EURUSD": 0.00012 }), resolved per cell at member construction.Basis: derived — the design ledger explicitly defers the currency→R normalization tier the pip route needs ("#124 collapses to recorded sidecar geometry → refuse in the meantime", the cost model being the named deferred consumer), so the map is the only shape whose semantics are fully decidable today; value-level (rather than a per-instrument cost list) lets one component vary per instrument while another stays scalar, which is the realistic case (spreads differ per instrument, a vol-slippage multiplier does not). The pip/currency-relative unit stays a #124-gated future extension, untouched by this cycle.
data.instruments): a map missing a campaign instrument is a fault naming the missing instruments; a key naming no campaign instrument is a fault naming the extras. No silent default, no partial coverage.Basis: derived — the issue's whole complaint is silent unit inconsistency; a partial map with an implicit zero or fallback for uncovered instruments would reproduce exactly that silence, and an extra key is almost always a symbol typo the run should refuse up front (the archive-inventory lesson of the Copper abort).
CostValuetype with custom serde mirroring the in-repoAxisprecedent: the scalar case serializes as the bare number, byte-identical to today'sf64field, so every stored scalar-shaped campaign document keeps its content id; only documents opting into the map form mint new ids. Noformat_versionbump (additive widening, per the schema's ownrisk/costprecedent).Basis: derived — content-id parity for unchanged documents is the C18 identity discipline; the
Axiscustom-(de)serializer is the proven in-repo pattern for a richer Rust type behind a bare wire form.Spec auto-signed (specify Step 6, autonomous run)
Spec: per-instrument-cost (each campaign cost knob takes one number or an instrument-keyed map, resolved per cell; strict intrinsic validation of map keys against the campaign instruments; scalar wire form byte-identical so stored documents keep their content ids).
The signature is the Step-5 grounding-check PASS — an independent fresh-context agent ratified twelve load-bearing assumption clusters against currently-green tests (report in the session transcript). No human signed; the fork decisions live at
issues/260#issuecomment-3535. Status: design settled, spec signed — proceeding to implementation planning.Plan-defect note for the audit trail (2026-07-14): the first implementation run of the per-instrument-cost plan blocked on its own fixture — the plan's inline validate-test JSON omitted the mandatory
presentationfield (CampaignDoc.presentationhas no serde default), which the loop's spec gate correctly refused as a byte-deviation the implementer was forced into. The plan was corrected (fixture carries the field) and re-run from a clean tree; no code from the blocked attempt was kept.