Currency book-tracking + position-event derive (Stage 2): deal = target - book - in_flight #115

Closed
opened 2026-06-22 16:30:50 +02:00 by Brummel · 3 comments
Owner

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.

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.
Brummel added this to the Realistic broker & position-event table (C10 A-side) milestone 2026-06-22 16:30:50 +02:00
Brummel added the feature label 2026-06-22 16:30:50 +02:00
Author
Owner

Design decisions (specify, cycle 0064 position-events-derive — derived)

Spec 0064-position-events-derive covers 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 pricederive consumes 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 pure
    fn takes the joined triples (unit tests supply hand-built triples).

  • Fork: crate placementSizingPolicy + lots() + derive_position_events
    live in aura-engine beside summarize; derive takes the instrument sizing
    scalars (contract_size, min_lot, lot_step), NOT InstrumentSpec.
    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. derive is
    a post-run reduction sibling of summarize and produces PositionEvent (already
    in 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
    PositionEvent there. The #116 broker's reuse of the shared sizing fn is
    deferred to the #116 cycle (it will import aura_engine's fn — adding an
    aura-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 below min_lot
    is dropped (no event). position_id monotonic, assigned at open, referenced by
    scale/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 tests
with hand-built streams. NO harness/CLI wiring (that is #123). NO broker (#116).

## Design decisions (specify, cycle 0064 position-events-derive — derived) Spec `0064-position-events-derive` covers 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** → `derive` consumes 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 pure fn takes the joined triples (unit tests supply hand-built triples). - **Fork: crate placement** → `SizingPolicy` + `lots()` + `derive_position_events` live in **aura-engine** beside `summarize`; `derive` takes the instrument sizing *scalars* (`contract_size`, `min_lot`, `lot_step`), NOT `InstrumentSpec`. 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. `derive` is a post-run reduction sibling of `summarize` and produces `PositionEvent` (already in 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 `PositionEvent` there. The **#116 broker's reuse** of the shared sizing fn is **deferred to the #116 cycle** (it will import `aura_engine`'s fn — adding an aura-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 below `min_lot` is **dropped** (no event). `position_id` monotonic, assigned at open, referenced by scale/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 tests with hand-built streams. NO harness/CLI wiring (that is #123). NO broker (#116).
Author
Owner

Design flaw found in the 0064 derive design — cycle bounced (specify/implement)

The derive design recorded in the comment above (volume = lots(|Δexposure|) at each
event's price; closes sized from the exposure delta) was implemented, passed the green
suite, then failed adversarial verification. Two confirmed defects:

  1. Orphaned/phantom lots from dropped sub-min partial adjustments (blocker). When a
    same-side partial adjustment's increment rounds below min_lot it is dropped (no
    event), but prev_e/open_id still advance — so the eventual close sizes against the
    exposure 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.950.05 lots
    orphaned
    (the book never flattens). The fix must size closes against the actual
    recorded open lots
    , not re-derive from |Δexposure|.

  2. 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-lots account·|e|/(cs·p1), so the
    table 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):

  • State: open_lots (real recorded position size), open_exposure (the exposure level
    that book corresponds to — diverges from the latest sample when a sub-min adjustment is
    deferred rather than fired), open_id.
  • open(flat→nonzero): lots(|e|@price); set book. scale-up: add lots(Δ|e|@price).
    scale-down/partial-close: close open_lots · (Δ|e| / |open_exposure|) (proportional).
    full close / reversal / force-close: close the whole open_lots.
  • sub-min adjustments defer (don't advance open_exposure) so small drifts accumulate
    until tradeable — the book never orphans.
  • Invariant (replaces the integrable-to-exposure claim): closes flatten the actual
    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).

## Design flaw found in the 0064 derive design — cycle bounced (specify/implement) The derive design recorded in the comment above (volume = `lots(|Δexposure|)` at each event's price; closes sized from the exposure delta) was implemented, passed the green suite, then **failed adversarial verification**. Two confirmed defects: 1. **Orphaned/phantom lots from dropped sub-min partial adjustments (blocker).** When a same-side partial adjustment's increment rounds below `min_lot` it is dropped (no event), but `prev_e`/`open_id` still advance — so the eventual close sizes against the exposure 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 lots orphaned** (the book never flattens). The fix must size closes against the *actual recorded open lots*, not re-derive from `|Δexposure|`. 2. **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-lots `account·|e|/(cs·p1)`, so the table 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): - State: `open_lots` (real recorded position size), `open_exposure` (the exposure level that book corresponds to — diverges from the latest sample when a sub-min adjustment is *deferred* rather than fired), `open_id`. - open(flat→nonzero): `lots(|e|@price)`; set book. scale-up: add `lots(Δ|e|@price)`. scale-down/partial-close: close `open_lots · (Δ|e| / |open_exposure|)` (proportional). full close / reversal / force-close: close the **whole** `open_lots`. - sub-min adjustments **defer** (don't advance `open_exposure`) so small drifts accumulate until tradeable — the book never orphans. - Invariant (replaces the integrable-to-exposure claim): **closes flatten the actual 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).
Brummel changed title from Sizing policy (fixed-notional) + derive_position_events (post-run first-difference) to Currency book-tracking + position-event derive (Stage 2): deal = target - book - in_flight 2026-06-23 13:17:49 +02:00
Author
Owner

Design 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 PositionManagement record +
summarize_r reduction. 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 book
    first-difference → PositionEvent table). 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 size that already exists.

  • Fork: input source. → the PositionManagement dense 14-column record,
    read positionally across the crate boundary, exactly as summarize_r does
    (sibling reduction in aura-engine, beside RunMetrics/summarize per #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_ts is the record
    row's cycle timestamp T
    , never the record's entry_ts column. On a reversal
    row the entry_ts/price columns describe the closed (old) trade while the
    position 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 backtest
    fills). Per row at T: if closed_this_cycle → emit Close(book.position_id, volume = book.volume) at T and clear the book; then if open and the book is
    empty → emit Buy/Sell per direction (col 4), volume = size (col 10),
    monotonic position_id, at T, and set the book. A reversal closes-then-opens
    at the same T in that order (matches #114). This is deal = target − book − in_flight for 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 = the size column at open; a
    Close carries the full book.volume (no partial closes in the current
    single-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_id monotonic from 0,
    assigned at each open; instrument_id passed into derive_position_events by
    the caller (from the run's InstrumentSpec.instrument_id), mirroring how
    summarize_r takes round_trip_cost as 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/Sell but no synthetic Close (the table records
    actual executed events). This differs from summarize_r, which force-closes
    an 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.

## Design 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 `PositionManagement` record + `summarize_r` reduction. 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 book first-difference → `PositionEvent` table). 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 `size` that already exists. - **Fork: input source.** → the `PositionManagement` dense 14-column record, read positionally across the crate boundary, exactly as `summarize_r` does (sibling reduction in aura-engine, beside `RunMetrics`/`summarize` per #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_ts` is the **record row's cycle timestamp T**, never the record's `entry_ts` column. On a reversal row the `entry_ts`/price columns describe the *closed* (old) trade while the position 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 backtest fills). Per row at T: if `closed_this_cycle` → emit `Close(book.position_id, volume = book.volume)` at T and clear the book; then if `open` and the book is empty → emit `Buy/Sell` per `direction` (col 4), volume = `size` (col 10), monotonic `position_id`, at T, and set the book. A reversal closes-then-opens at the same T in that order (matches #114). This is `deal = target − book − in_flight` for 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` = the `size` column at open; a Close carries the full `book.volume` (no partial closes in the current single-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_id` monotonic from 0, assigned at each open; `instrument_id` passed into `derive_position_events` by the caller (from the run's `InstrumentSpec.instrument_id`), mirroring how `summarize_r` takes `round_trip_cost` as 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/Sell` but **no synthetic Close** (the table records *actual* executed events). This differs from `summarize_r`, which force-closes an 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.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#115