predict: refresh NAV per market and flush from stored marks (DBU-557)#1116
Draft
0xaslan wants to merge 23 commits into
Draft
predict: refresh NAV per market and flush from stored marks (DBU-557)#11160xaslan wants to merge 23 commits into
0xaslan wants to merge 23 commits into
Conversation
The full-pool flush no longer walks any payout tree: a lifecycle-gated per-market refresh (settle-or-rebalance + exact liability walk) stores a ValuationMark on the market, and the flush reads marks off read-only markets under a freshness ceiling and a sqrt(min-total-variance)-scaled oracle-drift guard, then drains the LP queues unchanged. Trade flows write their exact liability delta through to the mark; the pool-wide valuation lock is removed; the payout-node cap drops to 950 so one market's refresh walk fits the per-transaction object budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y_nav Matches the priced trade flows: callers load the Pricer once per market via load_live_pricer and pass it in. A Pricer cannot outlive its transaction and cannot be loaded for a past-expiry market, so holding one proves the market is live here — the refresh's settle branch drops out, and terminal markets keep their existing sweep path (rebalance_expiry_cash). plp no longer touches the Block Scholes feed objects at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… ValuationMark (DBU-557) Review + design follow-ups on the draft: - Markets return raw flush atoms (free_cash, marked liability) with no per-market floor; the potato sums both and lp_pool_value applies the single zero floor at the end of the full chain. An underwater market (legitimate transient at backing lambda < 1) now nets against the pool instead of silently overstating the mark; FlushExecuted emits both raw sums so aggregate-underwater is observable. - Liquidation writes its removed live value through to the mark (review finding, High): knock-out fires above the floor, so a knocked-out order carries ~F*(1/ltv - 1) of value; all five liquidation paths now return it and decrement the mark. - ValuationMark moves to its own module owning construction, the freshness/drift acceptance, and write-through arithmetic; expiry_market keeps only the seams touching its private children. - Comment corrections: the falsified limited-recourse justification, the misleading no-liquidation-pass wording, plain-language drift-guard docs, and an explicit note on the wing-reshape blind spot (open item). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measured worst case over shape-preserving just-under-tolerance moves is 0.77-0.79 * epsilon of face (~2x the earlier ~0.4 estimate): 1.5% at the 0.02 default, ~8% at the 0.1 envelope ceiling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e hole
Measured verdict: SVI param-trading on the a + b*sigma*sqrt(1-rho^2) level
set (the observed BS refit regime-jump shape) repriced ATM strikes 84-89%
of face with both scalar legs passing on zero measured move, at any
epsilon; the real-data admitted-error tail was this hole firing. The mark
now stores sqrt total variance sampled at seven fixed log-moneyness probes
({-4,-2,-1,0,1,2,4} x anchor sqrt-min-variance); the flush re-samples the
live surface at the stored positions and rejects the mark when any probe
moves more than epsilon relative. Probe evaluation never aborts (clamped
wing term), so refresh and the mandatory flush stay live. Residual:
between-probe reshapes and deep wings (prices pinned near 0/1), documented
on VarianceProbes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sh reads, delta-accumulator mark - liquidate/liquidate_order return nothing (nothing consumed the count/bool on-chain; OrderLiquidated events carry the data), collapsing LiquidationSweep to a plain removed-live-value u64. - FlushAtoms replaced by two single-purpose reads: ungated live free_cash and gated flushable_marked_liability — each function returns the value it is named for. - ValuationMark keeps the walked liability immutable and accumulates trade write-through as added/removed deltas, netted once (saturating) at read: order-independent, no destructive per-op clamp, and the walk output stays auditable against a fresh walk for the invariant tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t tolerance
The mark now stores seven (strike, fair price) probe pairs fanned around
the refresh forward at {-4..+4} expected-moves; the flush reprices the
same strikes on the live surface and rejects the mark when any probe
price moved more than epsilon of full payout. One check subsumes the
forward, variance-floor, and shape legs: any oracle change that matters
to contract prices moves a probe, and the knob becomes the literal
face-error bound at the probes (no 0.8x conversion-factor analysis).
Deletes VarianceProbes, the scalar legs, and their error codes; caps the
strike exponent at e^2 so envelope-extreme surfaces cannot abort refresh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…level Restructure per the PLP-price framing: the guarantee we actually want is 'flush PLP price within a bound of the fresh-walk price', whose only correct denominator is pool NAV — so per-market acceptance is the wrong altitude. - valuation_mark is now a dumb tracker: liability walk + trade deltas + drift(pricer) measuring potential dollar NAV drift since the walk. It owns no acceptance policy and no config. - expiry_market exposes three dumb facts via individual getters: free_cash, marked_liability, mark_drift(pricer), plus mark_computed_at_ms. - plp aggregates all three per market, asserts the freshness ceiling per mark (the one guard a stalled feed cannot fool), and judges drift ONLY in aggregate at finish_flush: total_drift vs pool NAV. - Probe machinery removed from this head; it returns as the internal implementation of drift() when the model lands. INTERIM: drift() returns 0 and the aggregate check is a documented no-op — this head has freshness-only protection. Draft-only state; the drift model and threshold land next. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mark snapshots raw anchors at the walk (forward + SVI params; raw so the formula can evolve without stored state aging, and both sides derive through the same code at read time). drift(pricer) returns pricing::drift_envelope between anchor and live: an upper bound on any single contract's price move, in fractions of full payout, built from the normal curve's max slope (0.4) times a term-by-term bound on the worst d2 move — forward shift over the variance floor, plus one Lipschitz charge per SVI parameter delta over the strike band where prices are not pinned. Every term is zero when nothing moved; degenerate surfaces (zero floor, wing slope near 1, absurd band) return full face, fail-closed. Bound by construction: the surface IS these six numbers, so there is no strike between samples to hide a reshape in. Not yet charged (documented): the skew-correction term's own drift and the ulp tail beyond the pad — both to be priced by the envelope validation measurements. plp aggregation/enforcement unchanged (stub). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he bound The implemented price subtracts phi(d2) * w'(k) / (2 sqrt(w)); the clamp is 1-Lipschitz, so |dP| <= |dN(d2)| + |d correction|. Three new legs charge the correction's drift: phi_max times the w' change (per-parameter Lipschitz, sigma-floored), phi_max times the sqrt(w) change scaled by the correction's max height, and that height times the density's own move (phi's slope <= 0.25 times the already-bounded d2 move). Structural fix found during derivation: 'prices pinned outside |d2| = 4' is false for the skew-adjusted price — deep-wing corrections can pull prices off their pins (the P-11 mechanism) — so the band threshold now widens dynamically until phi(D) decays past the correction's max height. Hardening: the term assembly moves to u128 with a single fail-closed cap at full face, so envelope-corner surfaces saturate the drift instead of aborting mid-flush; sub-1e-4 variance floors (sub-seconds to expiry or degenerate surfaces) fail closed early. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… budget Completes the source contracts end to end. The payout tree already maintains the book's total face quantity in its treap summaries (every order contributes its quantity to exactly one start side), so the drift dollarization multiplier is a new O(1) getter over existing state — no counter, no walk change, and it reads live, covering post-anchor mints. mark_drift now returns dollars (live quantity x envelope fraction), and finish_flush enforces the budget for real: total accepted worst-case drift <= nav_mark_drift_epsilon x pool NAV, so the PLP price the queues fill at is within the configured fraction of the fresh-recalculation price (plus documented envelope residuals). FlushExecuted emits total_drift as the budget-headroom signal. Knob value unchanged (2%) pending the envelope-validation measurements; the pool_nav == 0 edge is documented as a deliberate open decision. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e threshold The flush mark becomes two-sided: supplies price the pool at mid NAV plus the counted marks' combined worst-case drift, withdrawals at mid minus it. The true pool value provably sits inside the interval, so the transacting party bears the marks' staleness, incumbents can be neither diluted nor drained, and a same-flush round trip strictly loses the spread — stale-NAV arbitrage dies by construction. Fully fresh marks collapse the spread to the exact single mark. Deleted: the aggregate drift threshold, its error code, and the nav_mark_drift_epsilon knob with its constants and setter — the spread is fully determined by the measured bound, so there is no tolerance left to configure. ValuationConfig shrinks to the freshness ceiling (the stalled- feed backstop no spread can replace). The pool_nav == 0 open edge dissolves: nothing asserts against a zero budget; a degenerate bid just refunds through the existing non-executable path. Wide spreads self- resolve through each request's own min-out limit (miss, carry, refund). Flush availability becomes unconditional. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er-coverage; add vault revision guard Review findings on the drift machinery (2 Critical understatements, 1 High isolation defect, 1 Medium abort path, comment drift): - CRITICAL anchor consistency: write-through deltas were valued at each op's own oracle, which endpoint drift cannot bound (an oracle round trip between refresh and flush leaves near-zero endpoint drift while a peak-priced mint stays in the sum). Deltas are now valued at the mark's STORED anchors: hooks pass the added/removed orders themselves and valuation_mark prices their ranges at anchor_svi/anchor_forward, so the marked liability is a single-anchor valuation of the current book and quantity x endpoint-envelope provably bounds it. Liquidation paths return the knocked-out Orders; redeems remove the original and re-add any partial-close remainder, both anchor-priced. - CRITICAL band under-coverage: unpinned_band's round-down chain could collapse the quadratic to W <= A and return K = 0 at low variance (review counterexample: drift understated ~13.6x). The band now takes max(quadratic + 10% + pad, analytic slope-free floor D*sqrt(A) + A/2 + 10% + pad) — over-covering is conservative-safe. - HIGH same-PTB isolation: with the valuation lock gone, a vault-cash op (rebalance/sweep/claim) composed between collection and finish could desynchronize collected atoms from live idle (double-count or omit moved cash). The ledger now carries a revision counter bumped on every cash movement; the potato snapshots it at start and finish_flush asserts it unchanged. Trade flows (account<->market cash) remain outside the guard: their collected-atom miss nets to fee dust. - MEDIUM: extreme forward ratios guard-return full face before math::div can overflow-abort (increase direction now fails closed like decrease). - Identical snapshots return exactly 0 drift (no tail pad), so an atomic refresh-and-flush PTB reproduces the old exact-single-mark behavior. - Stale comments: budget/threshold language updated to spread semantics; FlushExecuted doc re-attached to the right struct. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…band under-coverage; add vault revision guard" This reverts commit 88c1bf6.
With a real wing slope the true band W strictly exceeds A, so a computed w_minus_a of zero is always a round-down artifact (the low-variance ill-conditioned corner; reviewed counterexample understated drift 13.6x through an empty band). Detect it and return none — drift_envelope charges full face, the market prices at its worst case, and a refresh re-anchors it. No abort: the flush lands, the degenerate market's uncertainty is priced into the spread, and other markets are unaffected. Partial rounding shrinkage (band reduced but nonzero) is deliberately left to the envelope-validation measurement before any further hardening. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The flush potato reads each market's free cash at its collect and pool idle at finish. An internal cash move composed between those commands in the flush PTB itself (rebalance_expiry_cash is permissionless; PTB atomicity blocks everyone else) lands the moved amount in both reads or neither, double- or under-counting it in the priced NAV and letting a pre-queued LP request fill at the corrupted mark. Add a cash_revision counter on the vault Ledger, bumped inside the internal-transfer primitives (send_expiry_cash, receive_expiry_cash, realize_pending_protocol_profit) so an untracked move is unrepresentable. start_pool_valuation snapshots it into the potato; finish_flush asserts it unchanged (ECashMovedDuringValuation), aborting the malformed PTB. Mint/redeem stay exempt: they move external cash and its liability write-through in one bucket at one instant, coherent whenever read. Review finding 3 (flush cash isolation), approved disposition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… into strike_exposure The mark's write-through deltas were the op's own priced amounts (net_premium, redeem_amount, swept liquidation values). Endpoint drift between the stored anchors and the flush oracle cannot bound values priced at a third, intermediate oracle state: a round trip that spikes at trade time and returns by flush leaves near-zero endpoint drift while the peak-priced delta stays in the sum (review Critical 1). Re-express every delta in the anchor basis instead. An order's terms (boundary ticks, quantity, floor) fully determine its value at any oracle state, so hooks now value the affected orders at the mark's stored anchors: the marked liability stays exactly the current book valued at one snapshot, and quantity x endpoint-envelope provably bounds its revaluation. The op's execution price touches only cash, which the flush reads live and exact. pricing::from_anchors rebuilds a Pricer from the stored anchors, so the existing pricing surface (range_price, gross_order_value) is the single evaluator — no parallel anchor-pricing formula. The delta uses walk semantics (gross - floor, floored at zero), not order_value's holder-close zeroing of liquidatable orders: the walk carries a liquidatable-yet-unliquidated order's value until an actual knock-out removes it, and the write-through must round-trip the walk. The mark also moves from ExpiryMarket into StrikeExposure, next to the only state it tracks: every book mutation (mint insert, live-redeem close/replace, liquidation knock-out) now writes through inside one module, deleting expiry_market's six scattered hook sites and the u64 plumbing that threaded liquidation values out solely to feed the mark. ExpiryMarket keeps thin forwarders and the pricer-to-market binding check; the plp flush surface is unchanged. Review finding 1 (write-through anchoring), approved disposition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-land the uncontroversial remainder of the reverted review-fix package: - Identical snapshots return exactly 0 drift (no tail pad), so an atomic refresh-and-flush PTB reproduces the old exact-single-mark behavior. - Extreme forward ratios (>= 100x) guard-return full face in plain integer division before math::div can overflow-abort — the increase direction now fails closed like the decrease direction (review Medium 5). - FlushExecuted's doc block re-attached to its own struct (it sat above NavRefreshed) and updated with bid/ask half-spread semantics; collect_expiry_nav's doc updated from the dead drift-threshold language to spread semantics (review Low 7). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
supply_value/supply_executable (and the withdraw pair) were a data clump read together at exactly one site each, and the split shape let a future consumer price off the value without consulting the bool. Each side is now executable_*_value: Option<u64> — None when that side is non-executable this flush — so a degenerate mark cannot be priced by construction. Constructor derivation and quote/refund behavior unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lication, config fold Independent simplification review of the branch (comment directness + surface reduction); all ten findings applied: - Fix comments referencing code the branch itself renamed or deleted: flushable_atoms/FlushAtoms ghosts, plp::value_expiry (now collect_expiry_nav), FlushExecuted.total_drift pointing at the deleted drift-budget config, current_nav/finish_flush describing the pre-spread one-mark world. - Delete dead strike_exposure::total_live_quantity (zero callers since the mark moved in; mark_drift reads the tree aggregate directly). - Write each core design argument once at its owner and point elsewhere: anchor basis on ValuationMark, two-sided spread on FlushMark, pool-level floor in lp_pool_value, cash-move guard on PoolValuation.cash_revision. - Collapse the thrice-repeated mark-presence assert into one private mark() borrow helper; drop NavRefreshed field docs restating the struct doc; drop the tree getter's imported consumer rationale. - Rename the expiry_market forwarder record_valuation_mark -> refresh_valuation_mark to match the operation it delegates to. - Fold nav_mark_freshness_ms into ProtocolConfig as a bare validated field and delete the one-field valuation_config module — the second knob that licensed the struct (drift epsilon) was deleted when drift became the bid/ask spread. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ement) Generated fidelity vectors (drift_reference_data, 88 rows) + a parity test that seeds each vector's two oracle snapshots through the production ingest and asserts the chain's drift_envelope/up_price equal the DBU-557 measurement replica's outputs EXACTLY. Explicitly a two-sided consistency pin, not correctness coverage (pricing_exact_tests owns that via its independent erf reference): a green run proves the off-chain measurement computes what the chain computes, and any pricing.move change that shifts a covered value breaks loudly, flagging fixture regeneration. Coverage: zero early-out, every reachable fail-closed corner (incl. the round-down band collapse, found reachable in pricing-safe space), real captured SVI surfaces under perturbation ladders, one-ulp deltas, wing/grid strikes. Generator: predict-research scripts/drift_fixture_gen.py. Adds the missing set_bs_svi_for_testing fixture setter (lanes keep the newest source timestamp, so seeding a second surface snapshot needs an advanced one). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…vectors The DBU-557 sweep over 11 days of captured BTC oracle history found 15 exact-confirmed pairs where the chain's own fixed-point up_price moves MORE than drift_envelope allows — all in one regime: near-expiry 5m surfaces at ultra-low total variance (w ~ 1e-7, sigma at the pricing-safe minimum), identical SVI between snapshots, forward moves of ~2e-8 relative. Mechanism: one ulp of fixed-point total variance moves the price by ~phi(d2)*|k|/(2*w^1.5)*1e-9, which exceeds the envelope's 1.5e-4 tail pad below w ~ 5e-6; observed exceedances reach 4.3e-4 of face. These vectors embed the violating pairs (real forwards, violating strike), so this test now certifies the violating numbers through the production oracle ingest: |up0 - up1| > envelope on chain. The envelope needs a quantization-aware term or a higher variance floor — tracked with the measurement report. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
plp::refresh_expiry_nav: takes a transaction-loadedPricerlike the priced trade flows, rebalances one live market's cash, walks its payout tree once, and stores aValuationMark(exact per-order liability + oracle anchors + landing time) on theExpiryMarket. Several markets can share one refresh PTB. APricercannot exist for a past-expiry market, so refresh is live-markets-only by construction; terminal markets keep their existing sweep path (rebalance_expiry_cash).start_pool_valuationstill snapshots the active set into the hot-potato completeness proof, but the per-market step is nowcollect_expiry_nav(replacesvalue_expiry), which takes the market read-only plus a transaction-loadedPricerand returns raw atoms — live free cash and the marked liability, unfloored. The flush sums both across markets and applies a single zero floor at the pool level (lp_pool_value), so an underwater market (a legitimate transient whilebacking_buffer_lambda< 1: adverse redemptions can extract more cash than their λ-scaled backing reduction) nets against the pool instead of being silently floored to zero per market.FlushExecutedemits both raw sums, making aggregate-underwater observable.drift(pricer)— the potential dollar impact of oracle movement since its walk — and the flush aggregatesΣ driftacross markets and judges it once against pool NAV infinish_flush(assert_aggregate_drift_acceptable). This makes the guarantee direct:Σ drift ≤ threshold × pool NAVbounds the PLP-price error in its own units, which no per-market check can do (the per-contract → PLP translation floats with book size / pool equity). The drift measurement is a closed-form envelope (pricing::drift_envelope): the mark snapshots its walk's raw oracle anchors (forward + SVI params), anddrift(pricer)bounds the worst single-contract price move between anchor and live — the normal curve's max slope (0.4) times a term-by-term bound on the worst d2 move (forward shift over the variance floor + one Lipschitz charge per SVI parameter delta over the unpinned strike band). Bound by construction: the surface IS those six numbers, so no reshape can hide between samples — chosen after measured evidence that sampled probes admitted 84–89%-of-face wing reshapes. The skew-correction term's own drift is charged by three further legs (its tails also force a dynamically-widening band threshold — deep-wing corrections can pull prices off their 0/1 pins), and the term assembly runs in u128 with a single fail-closed cap so envelope-corner surfaces saturate instead of aborting mid-flush. Dollarization multiplies by the book's live face quantity — an O(1) read of aggregates the payout treap already maintains (drift moves face, not value: equal-liability books can differ 100× in sensitivity). The measured drift is priced into the flush mark as a bid/ask spread rather than enforced as a threshold: the mark is two-sided — supplies price the pool at mid NAV + Σ drift, withdrawals at mid − Σ drift — so the true pool value provably sits inside the interval, the transacting party bears the marks' staleness, incumbents can be neither diluted nor drained, and a same-flush round trip strictly loses the spread (stale-NAV arbitrage dies by construction). Fresh marks collapse the spread to an exact single mark. There is no drift tolerance knob at all — the spread is fully determined by the measured bound (ValuationConfigholds only the freshness ceiling, the stalled-feed backstop) — and flush availability is unconditional: a wide spread self-resolves through each request's existingmin_plp_out/min_dusdc_outlimit (miss, carry, refund).FlushExecutedemits the half-spread (total_drift).+net_premium,−redeem_amount,−removed_live_value). The liquidation leg was a review finding: knock-out fires atqty·P ≤ floor/ltv, i.e. while the order still carries~F·(1/ltv − 1)of live value, so every liquidation path (standalone entries and the sweeps inside mint/redeem) now returns the removed value and decrements the mark — without this the stored liability overstates and the supply queue over-mints. Cash moves need no maintenance because free cash is read live at flush. The mark struct and its freshness/drift acceptance policy live in a dedicatedvaluation_markmodule.valuation_in_progress) is removed along with all 18 guard sites — the flush is one atomic PTB over read-only markets, so the stored-mark acceptance checks replace the lock's coherence duty.ValuationConfig(nav_mark_freshness_msdefault 60s,nav_mark_drift_epsilondefault 0.02, both with envelope bounds), aNavRefreshedevent, andmax_payout_tree_nodeslowered 1,000 → 950.Why
dynamic_field::borrow_child_objectand LP queues cannot fill (predeploy C-1 capacity model; this is the measured object-cache wall, not a compute wall).Linear / Context
packages/predict/predeploy/open-items.md§ C-1 (capacity model + evidence).Key Decisions
0.4 * epsilonof face. Both queue directions still price at the same composed mark. Accepted and to be disclosed indocs/risks.mdwith the follow-up; this supersedes the earlierdecisions.mdrejection of a multi-tx valuation (its premise — one PTB can afford the full walk — is what C-1 falsified).MarketLifecycleProof): mark timing prices the queues, so refresh authority carries the flush's existing oracle-timing trust. For parallel refresh submission the cap can be held as a Sui party object — no Move change required.refresh_expiry_nav/rebalance_expiry_cashso it leaves the active set, then retry. Skipping it would undercount pool NAV while its cash is still inside the market.Scope / Descoped
value_expiryshape and will fail), harness/simulation TS +python_replayupdates (9 files call the old entrypoints), keeper service changes, predeploy/docs record updates (C-1 closure, response-policy entry for the lock removal, risks.md disclosure), and the capacity re-measurement that will finalize the cap values and default thresholds. Each lands as follow-up work under DBU-557 once the shape settles.Test Plan
sui move build --path packages/predict --warnings-are-errors— clean (exit 0; one pre-existing suppressed lint)git diff --check— cleanRisk / Rollout