Currency book-tracking + position-event derive (Stage 2): deal = target - book - in_flight #115
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?
Supersedes the rolled-back 0064 (fixed-notional sizing + exposure-integral derive, abandoned per #117). Stage-2 scope: derive the position-event table as the first difference of the BOOK via book-tracking (deal = target - book - in_flight; a close sizes the actual book, never an exposure delta). Fixed-fractional sizing reads account equity (compounding). Stays in the realistic-broker (Stage 2) milestone; entered only after Stage-1 E[R] > 0.
Design decisions (specify, cycle 0064 position-events-derive — derived)
Spec
0064-position-events-derivecovers this issue (#115). Running under/boss;the decisions below were derived by the orchestrator from the sources + ledger +
the crate dependency graph (not user decisions). Recorded for audit/veto.
Fork: sizing needs price →
deriveconsumes a joined(ts, exposure, price)sample stream, not exposure alone.
Basis (derived): "fixed-notional" means a fixed notional value
account·|exposure|;lots = notional / (contract_size · price)is price-dependent by definition. The#115 body wrote the formula but omitted price from the signature (the architect
flagged the undefined
contract_value).contract_value = contract_size · price.The caller joins the recorded exposure + price taps (via
join_on_ts); the purefn takes the joined triples (unit tests supply hand-built triples).
Fork: crate placement →
SizingPolicy+lots()+derive_position_eventslive in aura-engine beside
summarize;derivetakes the instrument sizingscalars (
contract_size,min_lot,lot_step), NOTInstrumentSpec.Basis (derived): the dependency graph is objective — aura-engine depends only on
aura-core (not aura-ingest), so it cannot import
InstrumentSpec(aura-ingest →aura-engine; the reverse is a cycle). The caller extracts the scalars.
deriveisa post-run reduction sibling of
summarizeand producesPositionEvent(alreadyin aura-engine, cycle 0063), so aura-engine is its natural home; the run loop never
calls it, so C14 (domain-free hot path) holds — the same standard that placed
PositionEventthere. The #116 broker's reuse of the shared sizing fn isdeferred to the #116 cycle (it will import
aura_engine's fn — adding anaura-std→aura-engine dep — or the fn relocates then, decided with broker context).
Not pre-committing the cross-crate coupling now is minimal-scope.
Fork: first-difference model → walk the exposure state; emit:
open (0→nonzero, Buy/Sell, new position_id) · close (nonzero→0) · scale-up
(same-sign |Δ| up = partial open) · scale-down (partial close) · reversal (sign
flip = Close then open at the same event_ts, close-before-open, new position_id
for the open) · force-close at window end (documented). Each event's
volume = lots(|Δexposure|)at the event price; an event whose lots round belowmin_lotis dropped (no event).
position_idmonotonic, assigned at open, referenced byscale/close.
Basis: the #115 body's 5 hand-worked test paths + the C10 "first difference of the
exposure state" definition.
Fork: audit table is broker-decoupled (Architecture A) → per-event
|Δexposure|-at-price sizing is coherent even though open/close lot volumes need not numerically
balance across a price-varying trajectory.
Basis: Architecture A (memory aura-broker-milestone-c10a + ledger C10) — the
realistic broker is a single-pass node on exposure+price, NOT a consumer of this
table; the table is a decoupled audit artifact, so it need not reconcile to a
broker's lot ledger.
Fork: account currency == quote currency this cycle (single-instrument
GER40/EUR); FX (quote≠account) deferred.
Basis: memory aura-broker-milestone-c10a (FX deferred).
Scope: pure functions (
SizingPolicy::lots+derive_position_events) + unit testswith hand-built streams. NO harness/CLI wiring (that is #123). NO broker (#116).
Design flaw found in the 0064 derive design — cycle bounced (specify/implement)
The derive design recorded in the comment above (volume =
lots(|Δexposure|)at eachevent's price; closes sized from the exposure delta) was implemented, passed the green
suite, then failed adversarial verification. Two confirmed defects:
Orphaned/phantom lots from dropped sub-min partial adjustments (blocker). When a
same-side partial adjustment's increment rounds below
min_lotit is dropped (noevent), but
prev_e/open_idstill advance — so the eventual close sizes against theexposure delta, not the lots actually on the book. Hand-traced:
e=[1.0, 0.95, 0.0]→Buy 1.0, (the −0.05 reduction is sub-min, dropped),Close 0.95→ 0.05 lotsorphaned (the book never flattens). The fix must size closes against the actual
recorded open lots, not re-derive from
|Δexposure|.Non-book-balancing under varying price. A position opened at price p0 and closed at
p1≠p0 has open-lots
account·|e|/(cs·p0)≠ close-lotsaccount·|e|/(cs·p1), so thetable reports a fully-closed position as still partly open. The artifact claims (ledger
C10 + doc + the E2E test) it is "the first difference of the exposure state" but
delivers non-integrable local-price lots. All fixtures used constant price + ≥min_lot
increments, masking both defects.
Recommended corrected design (track the actual book, not exposure deltas):
open_lots(real recorded position size),open_exposure(the exposure levelthat book corresponds to — diverges from the latest sample when a sub-min adjustment is
deferred rather than fired),
open_id.lots(|e|@price); set book. scale-up: addlots(Δ|e|@price).scale-down/partial-close: close
open_lots · (Δ|e| / |open_exposure|)(proportional).full close / reversal / force-close: close the whole
open_lots.open_exposure) so small drifts accumulateuntil tradeable — the book never orphans.
recorded open position (book-balances to 0); volumes are real lots (not integrable to
exposure under varying price — a structural first-difference, not an exposure integral).
The C10 ledger realization note + the doc/tests must state the book-balance invariant.
This reinterprets C10's "first difference of the exposure state" (structural event-log +
book-balance, NOT exposure reconstruction) and is milestone-foundational (volume semantics
feed #116/#118/#122) — bounced to the user to ratify before redoing the cycle. The 0064
spec+plan stay committed (HEAD 5e2f4db) pending that decision; the buggy impl was discarded
(working-tree only — main not rewound, per the project's main-sacrosanct rule).
Sizing policy (fixed-notional) + derive_position_events (post-run first-difference)to Currency book-tracking + position-event derive (Stage 2): deal = target - book - in_flightDesign reconciliation (specify) — cycle 0068, spec 0068-position-event-derive
Building #115 under /boss. The fork(s) below were derived from the C10 ledger
contract, the #114 schema, and the existing
PositionManagementrecord +summarize_rreduction. Recorded here as the run's decision log.Fork: scope — derive only, fixed-fractional sizing deferred to #116.
→ #115 delivers the post-run derive
derive_position_events(the bookfirst-difference →
PositionEventtable). The issue body also names"fixed-fractional sizing reads account equity (compounding)", but that needs
the equity→Sizer feedback, which C10 assigns explicitly to #116 (the z^-1
register on the fill edge). Building it here would require the broker+equity
of #116, a circular dependency.
Basis: derived — C10 puts the only feedback (equity→Sizer) in the Stage-2
broker/account layer (#116); the derive is standalone and testable against the
flat-1R
sizethat already exists.Fork: input source. → the
PositionManagementdense 14-column record,read positionally across the crate boundary, exactly as
summarize_rdoes(sibling reduction in aura-engine, beside
RunMetrics/summarizeper #114).Basis: derived — it is the only existing position-history source; matches #114's
"post-run reduction, same home as summarize".
Fork: event timestamp. → each emitted event's
event_tsis the recordrow's cycle timestamp T, never the record's
entry_tscolumn. On a reversalrow the
entry_ts/price columns describe the closed (old) trade while theposition is reopened the same cycle, so the column cannot supply the new leg's
open time; the row's own T is the correct open/close instant.
Basis: derived — forced by the PM reversal-row layout (verified in
position_management.rs eval blocks A/B/C).
Fork: book-tracking algorithm. → walk rows; maintain the derive's own
book: Option<{position_id, dir, volume}>(in_flight = 0, instant backtestfills). Per row at T: if
closed_this_cycle→ emitClose(book.position_id, volume = book.volume)at T and clear the book; then ifopenand the book isempty → emit
Buy/Sellperdirection(col 4), volume =size(col 10),monotonic
position_id, at T, and set the book. A reversal closes-then-opensat the same T in that order (matches #114). This is
deal = target − book − in_flightfor the single-executor book.Basis: derived — the C10 first-difference formulation specialised to the
current single-position-at-a-time executor; reversal close-before-open ordering
is the #114 contract.
Fork: volume + partial close. →
volume= thesizecolumn at open; aClose carries the full
book.volume(no partial closes in the currentsingle-position executor, though the schema supports partials).
Basis: derived — full close is faithful to the present executor; #114's partial
path stays schema-supported for future multi-leg work.
Fork: position_id + instrument_id. →
position_idmonotonic from 0,assigned at each open;
instrument_idpassed intoderive_position_eventsbythe caller (from the run's
InstrumentSpec.instrument_id), mirroring howsummarize_rtakesround_trip_costas a caller-supplied arg.Basis: derived — #114 fixes monotonic-at-open; instrument identity lives at the
source edge (aura-ingest), never in the engine hot path.
Fork: window-end open position. → a position still open on the last record
row emits its open
Buy/Sellbut no synthetic Close (the table recordsactual executed events). This differs from
summarize_r, which force-closesan open position at its unrealized R — that force-close is for the R metric, not
the event table.
Basis: derived — the position-event table is the executed book's first
difference; an in-window-unclosed position genuinely has no close event.