diff --git a/packages/predict/sources/config/config_constants.move b/packages/predict/sources/config/config_constants.move index c4452941f..5c3aa9073 100644 --- a/packages/predict/sources/config/config_constants.move +++ b/packages/predict/sources/config/config_constants.move @@ -30,6 +30,7 @@ const EInvalidBackingBufferLambda: u64 = 19; const EInvalidMaxAdmissionLeverage: u64 = 20; const EInvalidCadenceWindowSize: u64 = 21; const EMarketTickSizeTooLarge: u64 = 22; +const EInvalidNavMarkFreshnessMs: u64 = 23; // === Fees === @@ -359,3 +360,14 @@ public(package) fun assert_upper_benefit_power(value: u64) { EInvalidUpperBenefitPower, ); } + +public(package) macro fun default_nav_mark_freshness_ms(): u64 { 60_000 } +public(package) macro fun min_nav_mark_freshness_ms(): u64 { 1_000 } +public(package) macro fun max_nav_mark_freshness_ms(): u64 { 600_000 } + +public(package) fun assert_nav_mark_freshness_ms(value: u64) { + assert!( + value >= min_nav_mark_freshness_ms!() && value <= max_nav_mark_freshness_ms!(), + EInvalidNavMarkFreshnessMs, + ); +} diff --git a/packages/predict/sources/config/protocol_config.move b/packages/predict/sources/config/protocol_config.move index 3f172263e..dbb5a9e3d 100644 --- a/packages/predict/sources/config/protocol_config.move +++ b/packages/predict/sources/config/protocol_config.move @@ -3,9 +3,9 @@ /// Protocol-wide configuration and flow gates for Predict. /// -/// This shared object owns the admin-tunable config structs, the trading pause -/// gate, and the transaction-local full-pool valuation lock. Flow modules decide -/// which gates apply before they mutate expiry, oracle, pool, or account state. +/// This shared object owns the admin-tunable config structs and the trading pause +/// gate. Flow modules decide which gates apply before they mutate expiry, oracle, +/// pool, or account state. module deepbook_predict::protocol_config; use deepbook_predict::{ @@ -21,10 +21,8 @@ use deepbook_predict::{ }; const ETradingPaused: u64 = 0; -const EValuationInProgress: u64 = 1; -const EValuationNotInProgress: u64 = 2; -const EPackageVersionDisabled: u64 = 3; -const EVersionWatermarkNotAdvanced: u64 = 4; +const EPackageVersionDisabled: u64 = 1; +const EVersionWatermarkNotAdvanced: u64 = 2; /// Shared protocol policy and config state. public struct ProtocolConfig has key { @@ -47,9 +45,10 @@ public struct ProtocolConfig has key { version_watermark: u64, /// Blocks new risk creation while true. trading_paused: bool, - /// Transaction-local lock held while a full-pool valuation is assembled, so no - /// NAV-changing op can interleave between per-market value steps in the PTB. - valuation_in_progress: bool, + /// Hard ceiling on stored valuation-mark age at the pool flush, in + /// milliseconds — the backstop against a stalled feed, which shows zero + /// measured drift and so cannot be caught by the drift spread. + nav_mark_freshness_ms: u64, } // === Public Functions === @@ -165,7 +164,6 @@ public fun set_pyth_spot_freshness_ms( value: u64, ) { config.assert_version(); - config.assert_not_valuation_in_progress(); config.pricing_config.set_pyth_spot_freshness_ms(value); } @@ -176,7 +174,6 @@ public fun set_block_scholes_price_freshness_ms( value: u64, ) { config.assert_version(); - config.assert_not_valuation_in_progress(); config.pricing_config.set_block_scholes_price_freshness_ms(value); } @@ -187,10 +184,20 @@ public fun set_block_scholes_svi_freshness_ms( value: u64, ) { config.assert_version(); - config.assert_not_valuation_in_progress(); config.pricing_config.set_block_scholes_svi_freshness_ms(value); } +/// Set the hard freshness ceiling on stored valuation marks at the pool flush. +public fun set_nav_mark_freshness_ms( + config: &mut ProtocolConfig, + _admin_cap: &AdminCap, + value: u64, +) { + config.assert_version(); + config_constants::assert_nav_mark_freshness_ms(value); + config.nav_mark_freshness_ms = value; +} + /// Set the trading loss rebate rate template used by future expiry markets. public fun set_template_trading_loss_rebate_rate( config: &mut ProtocolConfig, @@ -260,7 +267,6 @@ public fun set_protocol_reserve_profit_share( protocol_reserve_profit_share: u64, ) { config.assert_version(); - config.assert_not_valuation_in_progress(); config_constants::assert_protocol_reserve_profit_share(protocol_reserve_profit_share); config.protocol_reserve_profit_share = protocol_reserve_profit_share; } @@ -305,6 +311,10 @@ public(package) fun ewma_config(config: &ProtocolConfig): &EwmaConfig { &config.ewma_config } +public(package) fun nav_mark_freshness_ms(config: &ProtocolConfig): u64 { + config.nav_mark_freshness_ms +} + /// Abort unless the running package version is at or above the watermark floor. /// /// The single version gate for the package: every version-gated flow threads the @@ -322,16 +332,6 @@ public(package) fun assert_trading_allowed(config: &ProtocolConfig) { config.assert_not_trading_paused(); } -/// Abort unless a valuation lock is currently active. -public(package) fun assert_valuation_in_progress(config: &ProtocolConfig) { - assert!(config.valuation_in_progress, EValuationNotInProgress); -} - -/// Abort unless no valuation lock is currently active. -public(package) fun assert_not_valuation_in_progress(config: &ProtocolConfig) { - assert!(!config.valuation_in_progress, EValuationInProgress); -} - /// Create and share the protocol-wide configuration object. public(package) fun create_and_share(ctx: &mut TxContext): ID { let config = new(ctx); @@ -346,18 +346,6 @@ public(package) fun pause_trading(config: &mut ProtocolConfig) { config.set_trading_paused_internal(true); } -/// Begin a transaction-local full-pool valuation lock. -public(package) fun begin_valuation(config: &mut ProtocolConfig) { - config.assert_not_valuation_in_progress(); - config.valuation_in_progress = true; -} - -/// End a transaction-local full-pool valuation lock. -public(package) fun end_valuation(config: &mut ProtocolConfig) { - config.assert_valuation_in_progress(); - config.valuation_in_progress = false; -} - fun set_trading_paused_internal(config: &mut ProtocolConfig, paused: bool) { config.trading_paused = paused; config_events::emit_trading_paused_updated(config.id(), paused); @@ -380,7 +368,7 @@ fun new(ctx: &mut TxContext): ProtocolConfig { ewma_config: ewma_config::new(), version_watermark: constants::current_version!(), trading_paused: false, - valuation_in_progress: false, + nav_mark_freshness_ms: config_constants::default_nav_mark_freshness_ms!(), } } diff --git a/packages/predict/sources/constants.move b/packages/predict/sources/constants.move index 0490e36d3..064f9f753 100644 --- a/packages/predict/sources/constants.move +++ b/packages/predict/sources/constants.move @@ -83,7 +83,10 @@ public(package) macro fun max_executable_plp_price(): u64 { 100_000_000 } public(package) macro fun max_live_expiry_markets(): u64 { 24 } /// Maximum finite payout-tree boundary nodes one expiry market may carry into NAV. -public(package) macro fun max_payout_tree_nodes(): u64 { 1_000 } +/// Held below Sui's ~1,000 cached-object per-transaction wall (measured single-market +/// crossing ~982 nodes, `predeploy/evidence/c1-object-cache-flush-2026-07-07.md`) so +/// one market's refresh walk always fits its own transaction. +public(package) macro fun max_payout_tree_nodes(): u64 { 950 } /// Maximum active leveraged orders one expiry market may carry into NAV. public(package) macro fun max_active_leveraged_orders(): u64 { 5_000 } @@ -157,10 +160,10 @@ public(package) macro fun min_fee_incentive_sponsorship(): u64 { 10_000_000 } /// resolution relayer sources that key from Pyth Lazer's exact-timestamp /// resolution endpoints and inserts it on this millisecond grid. /// Cadence periods are multiples of this value, so cadence-created expiries stay -/// on a settling key the relayer can produce. An off-grid expiry could never settle -/// and would block the pool flush indefinitely -/// (`plp::value_expiry` aborts on a past-expiry market that has no settling -/// observation yet). +/// on a settling key the relayer can produce. An off-grid expiry could never +/// settle and would block the pool flush indefinitely (a past-expiry market +/// cannot refresh its valuation mark — `pricing::load_live_pricer` rejects it — +/// so `plp::collect_expiry_nav` aborts stale until settlement sweeps it). public(package) macro fun resolution_period_ms(): u64 { one_minute_ms!() } // === Strike Tick Domain === diff --git a/packages/predict/sources/events/vault_events.move b/packages/predict/sources/events/vault_events.move index 5471ebee9..49384df24 100644 --- a/packages/predict/sources/events/vault_events.move +++ b/packages/predict/sources/events/vault_events.move @@ -152,21 +152,39 @@ public struct WithdrawFilled has copy, drop, store { dusdc_amount: u64, } +/// Emitted when a live market's valuation mark is refreshed: the exact +/// per-order liability stored for the pool flush to read, at its landing time. +public struct NavRefreshed has copy, drop, store { + pool_vault_id: ID, + expiry_market_id: ID, + liability: u64, + computed_at_ms: u64, +} + /// Emitted once per flush after both queues drain. The flush IS the full-pool -/// valuation, so this single event carries the frozen mark every fill was priced at -/// (`pool_value` over `total_supply`), its valuation breakdown (`idle_balance_before` -/// plus `active_market_nav` over `market_count` active markets), how many of each -/// kind filled, how many queue heads spent per-flush budget (filled, -/// protocol-refunded, or live limit-missed), and the idle balance after the drain. +/// valuation, so this single event carries the mid mark every fill was spread +/// around (`pool_value` over `total_supply`, with `total_drift` as the bid/ask +/// half-spread), its raw valuation breakdown (`idle_balance_before` plus +/// `total_free_cash` owing `total_liability` over `market_count` active +/// markets), how many of each kind filled, how many queue heads spent +/// per-flush budget (filled, protocol-refunded, or live limit-missed), and the +/// idle balance after the drain. public struct FlushExecuted has copy, drop, store { pool_vault_id: ID, epoch: u64, - /// LP-attributable pool NAV every fill was priced at: idle plus - /// `active_market_nav`, excluding unrealized and pending protocol profit. + /// LP-attributable pool NAV every fill was priced at: idle plus the raw + /// market sums below, excluding unrealized and pending protocol profit, + /// floored once at zero. pool_value: u64, total_supply: u64, - /// Σ of each active market's exact NAV at valuation (settled markets contribute 0). - active_market_nav: u64, + /// Σ of each counted market's live free cash at the flush (raw, unfloored; + /// with `total_liability` this exposes aggregate-underwater conditions). + total_free_cash: u64, + /// Σ of each counted market's marked liability at the flush (raw, unfloored). + total_liability: u64, + /// Σ of each counted market's worst-case dollar drift, applied at + /// `finish_flush` as the flush mark's bid/ask half-spread. + total_drift: u64, /// Number of active markets valued for this flush. market_count: u64, /// Idle DUSDC held by the pool at valuation time, before the drain. @@ -425,12 +443,28 @@ public(package) fun emit_withdraw_filled( }); } +public(package) fun emit_nav_refreshed( + pool_vault_id: ID, + expiry_market_id: ID, + liability: u64, + computed_at_ms: u64, +) { + event::emit(NavRefreshed { + pool_vault_id, + expiry_market_id, + liability, + computed_at_ms, + }); +} + public(package) fun emit_flush_executed( pool_vault_id: ID, epoch: u64, pool_value: u64, total_supply: u64, - active_market_nav: u64, + total_free_cash: u64, + total_liability: u64, + total_drift: u64, market_count: u64, idle_balance_before: u64, supplies_filled: u64, @@ -443,7 +477,9 @@ public(package) fun emit_flush_executed( epoch, pool_value, total_supply, - active_market_nav, + total_free_cash, + total_liability, + total_drift, market_count, idle_balance_before, supplies_filled, diff --git a/packages/predict/sources/expiry_market.move b/packages/predict/sources/expiry_market.move index 72a5b1b2b..95657aa8e 100644 --- a/packages/predict/sources/expiry_market.move +++ b/packages/predict/sources/expiry_market.move @@ -237,22 +237,18 @@ public fun load_live_pricer( /// negative. `load_live_pricer` binds the propbook feeds to this market's current /// Propbook registry mapping, rejects a past-expiry market, and gates oracle freshness. /// -/// A past-expiry market that has not settled cannot produce this pricer. There is -/// no solvency-safe NAV for an unsettled past-expiry market: the flush uses one -/// mark for both supply and withdraw, so the mark must equal the -/// settlement-dependent true value. Flows that branch on settlement call -/// `ensure_settled` first, using Propbook's exact Pyth timestamp at expiry; if no -/// exact spot exists yet, the live-pricing liveness abort remains the correct -/// failure mode. +/// A past-expiry market that has not settled cannot produce this pricer: its +/// true value is settlement-dependent, so it has no solvency-safe live NAV. +/// Flows that branch on settlement call `ensure_settled` first, using +/// Propbook's exact Pyth timestamp at expiry; if no exact spot exists yet, the +/// live-pricing liveness abort remains the correct failure mode. public fun current_nav(market: &ExpiryMarket, pricer: &Pricer): u64 { market.assert_pricer_bound(pricer); let liability = market.strike_exposure.exact_live_liability(pricer); - // Floor at 0 rather than abort: a degenerate underwater market has zero - // limited-recourse value, and partial-close `walk_linear` survivors can leave - // residual ulp dust that makes liability exceed free cash by ~1-2 ulp/order. - // This is a ROUNDING_POLICY R1/R2 liveness/dust clamp, not a conservative - // supply mark: a lower pool mark would mint more PLP to new suppliers, so the - // exact-mark invariant remains the governing safety property. + // Floor at 0: a devInspect/SDK read of one market's limited-recourse value + // is never negative. The flush does not read this — it sums each market's + // unfloored free cash and marked liability and floors once at the pool + // (`plp::lp_pool_value`). market.cash.free_cash().saturating_sub(liability) } @@ -610,31 +606,32 @@ public fun redeem_settled_permissionless( /// Run one bounded liquidation pass over active leveraged orders. /// -/// The liquidation book selects up to `budget` candidates and returns the -/// number of orders liquidated. It does not touch accounts; users clear -/// their liquidated position later through `redeem_live` or `redeem_settled`, +/// The liquidation book selects up to `budget` candidates; each knock-out emits +/// an `OrderLiquidated` event. It does not touch accounts; users clear their +/// liquidated position later through `redeem_live` or `redeem_settled`, /// receiving no payout. public fun liquidate( market: &mut ExpiryMarket, config: &ProtocolConfig, pricer: &Pricer, budget: u64, -): u64 { +) { market.assert_live_flow_allowed(config, pricer); - market.strike_exposure.liquidate_live_orders(pricer, budget) + market.strike_exposure.liquidate_live_orders(pricer, budget); } -/// Try to liquidate one active leveraged order by ID. +/// Try to liquidate one active leveraged order by ID; a knock-out emits an +/// `OrderLiquidated` event, and an ineligible order is a no-op. public fun liquidate_order( market: &mut ExpiryMarket, config: &ProtocolConfig, pricer: &Pricer, order_id: u256, -): bool { +) { market.assert_live_flow_allowed(config, pricer); let order = order::from_order_id(order_id); - market.strike_exposure.liquidate_live_order(pricer, &order) + market.strike_exposure.liquidate_live_order(pricer, &order); } /// Set this expiry's reference fine-grid tick from the exact previous-window @@ -648,7 +645,6 @@ public fun set_reference_tick( pyth: &PythFeed, ): u64 { config.assert_version(); - config.assert_not_valuation_in_progress(); market.assert_pyth_bound(propbook_registry, pyth); let source_timestamp_ms = market.strike_exposure.reference_tick_source_timestamp_ms(); @@ -719,6 +715,43 @@ public(package) fun ensure_settled( true } +/// Return this market's live free cash (DUSDC custody net of the rebate +/// reserve) — always current, no gate needed; cash moves never stale it. +public(package) fun free_cash(market: &ExpiryMarket): u64 { + market.cash.free_cash() +} + +/// Return the stored mark's current liability +/// (`strike_exposure::marked_liability` owns the semantics). +public(package) fun marked_liability(market: &ExpiryMarket): u64 { + market.strike_exposure.marked_liability() +} + +/// Return the landing time of the refresh that computed the stored mark. +public(package) fun mark_computed_at_ms(market: &ExpiryMarket): u64 { + market.strike_exposure.mark_computed_at_ms() +} + +/// Measure the stored mark's potential oracle drift against the live inputs in +/// `pricer`, in DUSDC base units (`strike_exposure::mark_drift` owns the +/// semantics; this composition point owns the pricer-to-market binding check). +public(package) fun mark_drift(market: &ExpiryMarket, pricer: &Pricer): u64 { + market.assert_pricer_bound(pricer); + market.strike_exposure.mark_drift(pricer) +} + +/// Recompute this market's exact per-order live liability and store it as the +/// valuation mark the pool flush reads, replacing any prior mark. Returns the +/// stored liability. +public(package) fun refresh_valuation_mark( + market: &mut ExpiryMarket, + pricer: &Pricer, + clock: &Clock, +): u64 { + market.assert_pricer_bound(pricer); + market.strike_exposure.refresh_valuation_mark(pricer, clock) +} + /// Force `mint_paused = true`. Reserved for `PauseCap` holders going through /// `registry::pause_expiry_market_mint_pause_cap`; cannot unpause. Deliberately /// not version-gated so the kill switch survives a version freeze. @@ -916,13 +949,11 @@ fun assert_pyth_bound(market: &ExpiryMarket, propbook_registry: &OracleRegistry, fun assert_live_flow_allowed(market: &ExpiryMarket, config: &ProtocolConfig, pricer: &Pricer) { config.assert_version(); - config.assert_not_valuation_in_progress(); market.assert_pricer_bound(pricer); } fun assert_live_mint_allowed(market: &ExpiryMarket, config: &ProtocolConfig, pricer: &Pricer) { config.assert_version(); - config.assert_not_valuation_in_progress(); market.assert_pricer_bound(pricer); config.assert_trading_allowed(); assert!(!market.mint_paused, EMintPaused); @@ -1173,7 +1204,6 @@ fun redeem_settled_internal( ctx: &mut TxContext, ): (u256, Option) { config.assert_version(); - config.assert_not_valuation_in_progress(); let redeemed_order = order::from_order_id(order_id); assert!(close_quantity == redeemed_order.quantity(), EFullCloseRequired); assert!(market.ensure_settled(propbook_registry, pyth, clock), EMarketNotSettled); diff --git a/packages/predict/sources/plp/lp_book.move b/packages/predict/sources/plp/lp_book.move index d0feaffce..d0d876eb6 100644 --- a/packages/predict/sources/plp/lp_book.move +++ b/packages/predict/sources/plp/lp_book.move @@ -67,13 +67,24 @@ public struct RequestQueue has store { escrow: Balance, } -/// Frozen flush share-price mark: `pool_value` over `total_supply`. Carried as one -/// value so the supply/withdraw pricing helpers read each term by name and cannot -/// transpose the numerator and denominator. +/// Frozen flush share-price mark, two-sided: supplies price the pool at +/// `supply_value` (the mid NAV plus the flush's measured worst-case drift) and +/// withdrawals at `withdraw_value` (the mid minus it). The true pool value +/// provably sits inside that interval, so each transactor prices as if the +/// pool were worth the end least favorable to them — the party moving bears +/// the marks' staleness, incumbents can be neither diluted nor drained, and a +/// same-flush round trip strictly loses the spread. Fully fresh marks collapse +/// the spread to a single exact mark. Executability is per side, in the type: +/// a side's value is `None` when that side is not executable this flush (one +/// side can refund while the other fills), so no consumer can price off a +/// degenerate mark. Terms are read by name so the pricing helpers cannot +/// transpose numerator and denominator. public struct FlushMark has drop { - pool_value: u64, + /// Mid NAV plus the flush's measured worst-case drift. + executable_supply_value: Option, + /// Mid NAV minus the flush's measured worst-case drift. + executable_withdraw_value: Option, total_supply: u64, - executable: bool, } /// Result of draining both LP queues at one frozen mark. @@ -153,11 +164,15 @@ public(package) fun cancel_withdraw_request( (request.account_id, request.amount, refund) } -public(package) fun new_flush_mark(pool_value: u64, total_supply: u64): FlushMark { +public(package) fun new_flush_mark( + supply_value: u64, + withdraw_value: u64, + total_supply: u64, +): FlushMark { FlushMark { - pool_value, + executable_supply_value: executable_mark_value(supply_value, total_supply), + executable_withdraw_value: executable_mark_value(withdraw_value, total_supply), total_supply, - executable: is_executable_mark(pool_value, total_supply), } } @@ -579,30 +594,36 @@ fun entry_offset(entries: &vector, index: u64): u64 { // === Pricing Helpers === -/// LP shares minted for `amount` DUSDC at the frozen flush mark. `None` means the +/// LP shares minted for `amount` DUSDC at the mark's supply side. `None` means the /// mark/request pair is not executable and the queued request must be refunded. fun quote_supply_shares(mark: &FlushMark, amount: u64): Option { - if (!mark.executable) return option::none(); - // = amount * total_supply / pool_value, round down (supplier mints ≤1 ulp - // fewer shares; the pool keeps the dust). - math::try_mul_div_down(amount, mark.total_supply, mark.pool_value).and!(|shares| { - if (shares == 0) option::none() else option::some(shares) + mark.executable_supply_value.and_ref!(|supply_value| { + // = amount * total_supply / supply_value, round down (supplier mints ≤1 ulp + // fewer shares; the pool keeps the dust). + math::try_mul_div_down(amount, mark.total_supply, *supply_value).and!(|shares| { + if (shares == 0) option::none() else option::some(shares) + }) }) } -/// DUSDC owed for `shares` LP at the frozen flush mark. `None` means the +/// DUSDC owed for `shares` LP at the mark's withdraw side. `None` means the /// mark/request pair is not executable and the queued request must be refunded. fun quote_withdraw_dusdc(mark: &FlushMark, shares: u64): Option { - if (!mark.executable) return option::none(); - // = shares * pool_value / total_supply, round down (withdrawer is paid ≤1 ulp - // less; the pool keeps the dust). - math::try_mul_div_down(shares, mark.pool_value, mark.total_supply).and!(|payout| { - if (payout == 0) option::none() else option::some(payout) + mark.executable_withdraw_value.and_ref!(|withdraw_value| { + // = shares * withdraw_value / total_supply, round down (withdrawer is paid + // ≤1 ulp less; the pool keeps the dust). + math::try_mul_div_down(shares, *withdraw_value, mark.total_supply).and!(|payout| { + if (payout == 0) option::none() else option::some(payout) + }) }) } -fun is_executable_mark(pool_value: u64, total_supply: u64): bool { - if (total_supply == 0) return false; +/// `Some(pool_value)` when a queue side may execute at this value: a non-empty +/// supply whose per-share price computes cleanly on both rounding sides and +/// stays inside the executable price envelope; `None` marks the side +/// non-executable for the whole flush. +fun executable_mark_value(pool_value: u64, total_supply: u64): Option { + if (total_supply == 0) return option::none(); let price_floor = math::try_mul_div_down( pool_value, constants::plp_price_unit!(), @@ -613,8 +634,10 @@ fun is_executable_mark(pool_value: u64, total_supply: u64): bool { constants::plp_price_unit!(), total_supply, ); - price_floor.is_some() + let executable = + price_floor.is_some() && price_ceil.is_some() && *price_floor.borrow() >= constants::min_executable_plp_price!() - && *price_ceil.borrow() <= constants::max_executable_plp_price!() + && *price_ceil.borrow() <= constants::max_executable_plp_price!(); + if (executable) option::some(pool_value) else option::none() } diff --git a/packages/predict/sources/plp/plp.move b/packages/predict/sources/plp/plp.move index 53931d888..8edabeece 100644 --- a/packages/predict/sources/plp/plp.move +++ b/packages/predict/sources/plp/plp.move @@ -5,11 +5,13 @@ /// /// PoolVault owns the PLP treasury cap, the pooled DEEP staked by accounts, idle /// DUSDC, the protocol reserve, sponsor-funded fee incentives, per-expiry cash -/// accounting, and the async LP supply/withdraw queues. It coordinates the -/// full-pool NAV valuation (a hot-potato aggregation over every active market) and +/// accounting, and the async LP supply/withdraw queues. It coordinates per-market +/// NAV refreshes (`refresh_expiry_nav` walks one market's payout tree and stores a +/// valuation mark on it), the full-pool flush (a hot-potato completeness proof +/// that reads every active market's stored mark without walking any tree), and /// the unified per-market cash flow (initial funding, live rebalance/sweep, and /// settled-market sweep with terminal profit materialization). LPs queue -/// supply/withdraw requests routed through a loaded Account; the daily flush +/// supply/withdraw requests routed through a loaded Account; the flush /// (`finish_flush`) drains them at the frozen pool NAV, minting/burning PLP and /// delivering fills to each account via the balance accumulator. PLP incentives /// moved to a separate staking contract; DEEP staking is an unrelated trading @@ -25,18 +27,13 @@ use deepbook_predict::{ market_lifecycle_cap::MarketLifecycleProof, pool_accounting::{Self, Ledger}, predict_account, + pricing::Pricer, protocol_config::ProtocolConfig, vault_events }; use dusdc::dusdc::DUSDC; use fixed_math::math; -use propbook::{ - block_scholes_forward_feed::BlockScholesForwardFeed, - block_scholes_spot_feed::BlockScholesSpotFeed, - block_scholes_svi_feed::BlockScholesSVIFeed, - pyth_feed::PythFeed, - registry::OracleRegistry -}; +use propbook::{pyth_feed::PythFeed, registry::OracleRegistry}; use sui::{ accumulator::AccumulatorRoot, balance::{Self, Balance}, @@ -56,6 +53,8 @@ const EBelowMinBootstrapLiquidity: u64 = 6; const EBelowMinFeeIncentiveSponsorship: u64 = 7; const EMarketNotSettled: u64 = 8; const EMaxLiveExpiryMarketsExceeded: u64 = 9; +const EValuationMarkStale: u64 = 10; +const ECashMovedDuringValuation: u64 = 11; /// One-time witness type for Predict LP token registration. public struct PLP has drop {} @@ -77,21 +76,36 @@ public struct PoolVault has key { expiry_accounting: Ledger, } -/// Transaction-local full-pool NAV valuation hot potato. +/// Transaction-local full-pool flush completeness proof. /// -/// `start_pool_valuation` snapshots the active expiry set; each `value_expiry` -/// runs the per-market cash flush then folds that market's NAV into `total_nav` -/// exactly once (a swept settled market contributes 0); `finish_flush` proves every -/// snapshotted market was valued, returns the LP-attributable pool NAV, and drains -/// the LP queues against it. Has no abilities, so it must be consumed by the finisher. +/// `start_pool_valuation` snapshots the active expiry set; each `collect_expiry_nav` +/// folds one market's stored valuation mark into `total_nav` exactly once, reading +/// the market read-only (the payout-tree walk ran in that market's +/// `refresh_expiry_nav`, so the flush loads no per-order objects and stays under +/// the per-transaction object budget); `finish_flush` proves every snapshotted +/// market was counted, returns the LP-attributable pool NAV, and drains the LP +/// queues against it. Has no abilities, so it must be consumed by the finisher. public struct PoolValuation { pool_vault_id: ID, - /// Active expiry markets snapshotted at start; every one must be valued. + /// Active expiry markets snapshotted at start; every one must be counted. expected_expiry_markets: vector, - /// Markets valued so far this flow; folded against `expected` at finish. + /// Markets counted so far this flow; folded against `expected` at finish. valued_expiry_markets: vector, - /// Running Σ of each valued market's NAV (settled markets contribute 0). - total_nav: u64, + /// Running Σ of each counted market's live free cash — raw and unfloored, + /// so underwater markets net against the whole pool; the single zero floor + /// is `lp_pool_value`'s. + total_free_cash: u64, + /// Running Σ of each counted market's marked liability. + total_liability: u64, + /// Running Σ of each counted market's measured worst-case dollar drift; + /// becomes the flush mark's bid/ask half-spread at `finish_flush`. + total_drift: u64, + /// The vault ledger's cash revision at `start_pool_valuation`. `finish_flush` + /// asserts it unchanged: the potato reads each market's cash at its collect + /// and idle at finish, so an internal cash move interleaved between those + /// reads (only the flush PTB's own commands can do this — PTBs are atomic) + /// would double- or under-count the moved amount in the priced NAV. + cash_revision: u64, } // === Package Initializer === @@ -203,87 +217,121 @@ public fun pending_protocol_profit(vault: &PoolVault): u64 { vault.expiry_accounting.pending_protocol_profit() } -/// Begin a full-pool flush (NAV valuation + LP queue drain) as a market deployer, -/// using a registry-generated `MarketLifecycleProof`. This is the sole flush start: -/// it is cron-driven and PRIVILEGED, not permissionless (audit L8). Engages the -/// protocol valuation lock — so no NAV-changing op can interleave between value -/// steps — and snapshots the active expiry set every `value_expiry` must cover. The -/// hot potato can only be created here, so gating the start gates the whole flush. +/// Refresh one live market's stored valuation mark as a market deployer, using a +/// registry-generated `MarketLifecycleProof` and a `Pricer` loaded in this +/// transaction (`expiry_market::load_live_pricer`, like every priced flow). The +/// market is rebalanced to target, its exact per-order liability recomputed at +/// the pricer's oracle inputs, and the mark stored for `collect_expiry_nav` to +/// read. Several markets may be refreshed in one PTB subject to the +/// per-transaction object budget (each refresh walks its market's payout tree). +/// +/// Live-markets-only by construction: a `Pricer` cannot outlive its transaction +/// and `load_live_pricer` rejects past-expiry markets, so holding one proves the +/// market is pre-expiry here — no settle branch needed. Terminal markets are +/// settled and swept out of the active set by `rebalance_expiry_cash`. +/// +/// PRIVILEGED like the flush start (audit L8): the mark's refresh instant prices +/// the pool NAV the queues later drain at, and Pyth updates are permissionless — +/// so refresh authority carries the same oracle-timing trust as the flush itself. +public fun refresh_expiry_nav( + vault: &mut PoolVault, + market: &mut ExpiryMarket, + lifecycle_proof: MarketLifecycleProof, + config: &ProtocolConfig, + pricer: &Pricer, + clock: &Clock, +) { + config.assert_version(); + lifecycle_proof.destroy_proof(); + let expiry_market_id = market.id(); + vault.expiry_accounting.assert_registered_expiry(expiry_market_id); + vault.rebalance_live_expiry(market, expiry_market_id); + let liability = market.refresh_valuation_mark(pricer, clock); + vault_events::emit_nav_refreshed( + vault.id(), + expiry_market_id, + liability, + clock.timestamp_ms(), + ); +} + +/// Begin a full-pool flush (stored-mark aggregation + LP queue drain) as a market +/// deployer, using a registry-generated `MarketLifecycleProof`. This is the sole +/// flush start: it is cron-driven and PRIVILEGED, not permissionless (audit L8). +/// Snapshots the active expiry set every `collect_expiry_nav` must cover; the hot +/// potato can only be created here, so gating the start gates the whole flush. /// -/// The flush prices the pool NAV off the live oracle and `finish_flush` drains the -/// LP queues at that mark, and Pyth updates (`pyth_feed::update`) are permissionless -/// — so a flush-capable cap-holder who manipulates the live oracle in a preceding -/// tx, then flushes, could fill their own queued supply/withdraw request at a mark -/// they chose. The start is therefore gated on both current registry allowlisting -/// and trust in every flush-capable holder not to manipulate the live oracle. The -/// revocable `MarketLifecycleCap` (not the root `AdminCap`) carries this authority; -/// admin retains a break-glass route by minting itself a lifecycle cap. +/// The flush prices the pool NAV off marks refreshed at operator-chosen instants +/// and `finish_flush` drains the LP queues at that mark, and Pyth updates +/// (`pyth_feed::update`) are permissionless — so a cap-holder who manipulates the +/// live oracle before refreshing, then flushes, could fill their own queued +/// supply/withdraw request at a mark they chose. The start is therefore gated on +/// both current registry allowlisting and trust in every flush-capable holder not +/// to manipulate the live oracle. The revocable `MarketLifecycleCap` (not the root +/// `AdminCap`) carries this authority; admin retains a break-glass route by +/// minting itself a lifecycle cap. public fun start_pool_valuation( - config: &mut ProtocolConfig, + config: &ProtocolConfig, vault: &PoolVault, lifecycle_proof: MarketLifecycleProof, ): PoolValuation { config.assert_version(); lifecycle_proof.destroy_proof(); - start_pool_valuation_internal(config, vault) + assert!(vault.lp.total_supply() > 0, ENotBootstrapped); + PoolValuation { + pool_vault_id: vault.id(), + expected_expiry_markets: vault.expiry_accounting.active_expiry_markets(), + valued_expiry_markets: vector[], + total_free_cash: 0, + total_liability: 0, + total_drift: 0, + cash_revision: vault.expiry_accounting.cash_revision(), + } } -/// Run the per-market cash flow for one snapshotted market, then fold its NAV into -/// the running total. The market must be in the snapshot and not already valued -/// (the exactly-once proof). The flush IS the valuation: a settled market is swept -/// (deactivated, cash returned, profit materialized) and contributes 0; a live -/// market is rebalanced to target and valued on its current cash. +/// Fold one snapshotted market's flush facts into the running pool aggregates, +/// using a `Pricer` loaded in this transaction (the live oracle inputs its +/// drift is measured against). The market must be in the snapshot and not +/// already counted (the exactly-once proof), and is a READ-ONLY input: no cash +/// moves, no settlement, no tree walk. Three facts are collected — live free +/// cash, the stored marked liability, and the mark's measured dollar drift — +/// and only one gate applies here: the mark must be younger than the freshness +/// ceiling (the sole guard a stalled feed cannot fool). Drift is never +/// rejected — `finish_flush` prices the aggregate as the flush mark's bid/ask +/// half-spread, borne by the transacting party. /// -/// Before branching, this passively records terminal settlement from Propbook's -/// exact Pyth timestamp if available: a past-expiry market is normally settled here -/// and swept (contributing 0), so `current_nav` is only reached for a still-live -/// market. Only in the bounded pending-settlement window (past expiry but the -/// exact-expiry spot not yet inserted) does the live branch still abort through -/// `current_nav`; there is no solvency-safe substitute mark for an unsettled expired -/// market, and the abort clears once anyone lands the exact spot. -public fun value_expiry( +/// A settled or past-expiry market cannot produce the pricer this read requires +/// (`load_live_pricer` rejects past-expiry): sweep it via `rebalance_expiry_cash` +/// so it leaves the active set, then start a new flush. There is still no +/// solvency-safe substitute mark for an unsettled expired market; the abort +/// clears once anyone lands the exact spot. +public fun collect_expiry_nav( valuation: &mut PoolValuation, - vault: &mut PoolVault, - market: &mut ExpiryMarket, + market: &ExpiryMarket, config: &ProtocolConfig, - propbook_registry: &OracleRegistry, - pyth: &PythFeed, - bs_spot: &BlockScholesSpotFeed, - bs_forward: &BlockScholesForwardFeed, - bs_svi: &BlockScholesSVIFeed, + pricer: &Pricer, clock: &Clock, ) { config.assert_version(); - config.assert_valuation_in_progress(); let expiry_market_id = market.id(); valuation.assert_expiry_ready_to_value(expiry_market_id); - vault.expiry_accounting.assert_registered_expiry(expiry_market_id); - let nav = if ( - vault.settle_or_rebalance_expiry(market, config, propbook_registry, pyth, clock) - ) { - 0 - } else { - let pricer = market.load_live_pricer( - config, - propbook_registry, - pyth, - bs_spot, - bs_forward, - bs_svi, - clock, - ); - market.current_nav(&pricer) - }; + assert!( + clock.timestamp_ms() - market.mark_computed_at_ms() + <= config.nav_mark_freshness_ms(), + EValuationMarkStale, + ); valuation.valued_expiry_markets.push_back(expiry_market_id); - valuation.total_nav = valuation.total_nav + nav; + valuation.total_free_cash = valuation.total_free_cash + market.free_cash(); + valuation.total_liability = valuation.total_liability + market.marked_liability(); + valuation.total_drift = valuation.total_drift + market.mark_drift(pricer); } /// Finish a full-pool valuation and run the LP flush: prove every snapshotted market -/// was valued exactly once, price the pool NAV, then drain the supply/withdraw queues +/// was counted exactly once, price the pool NAV, then drain the supply/withdraw queues /// at that frozen mark (mint PLP for supplies, burn PLP and pay DUSDC for -/// withdrawals), release the valuation lock, consume the potato, and return the -/// LP-attributable pool-wide DUSDC NAV (idle + Σ active NAV, net of the -/// pending-protocol-profit exclusion priced from the aggregate profit basis). +/// withdrawals), consume the potato, and return the LP-attributable pool-wide DUSDC +/// NAV (idle + Σ free cash − Σ marked liability, net of the pending-protocol-profit +/// exclusion priced from the aggregate profit basis). /// /// `supply_budget` / `withdraw_budget` bound how many requests each queue may /// process this flush (`None` = unbounded). Filled heads, protocol-refunded @@ -295,35 +343,48 @@ public fun value_expiry( public fun finish_flush( valuation: PoolValuation, vault: &mut PoolVault, - config: &mut ProtocolConfig, + config: &ProtocolConfig, supply_budget: Option, withdraw_budget: Option, ctx: &mut TxContext, ): u64 { config.assert_version(); - config.assert_valuation_in_progress(); valuation.assert_pool_vault(vault); assert_all_expected_valued( &valuation.expected_expiry_markets, &valuation.valued_expiry_markets, ); - let PoolValuation { total_nav, valued_expiry_markets, .. } = valuation; + assert!( + vault.expiry_accounting.cash_revision() == valuation.cash_revision, + ECashMovedDuringValuation, + ); + let PoolValuation { + total_free_cash, + total_liability, + total_drift, + valued_expiry_markets, + .., + } = valuation; let idle_balance_before = vault.expiry_accounting.idle_balance(); let pool_nav = lp_pool_value( vault, config.protocol_reserve_profit_share(), - total_nav, + total_free_cash, + total_liability, ); let total_supply = vault.lp.total_supply(); let market_count = valued_expiry_markets.length(); - // Snapshot the share price once (frozen pair), drain both queues against it, then - // release the valuation lock at the very end. The flush IS the full-pool - // valuation, so the single FlushExecuted event carries the priced mark and its - // idle + active-NAV breakdown. + // Two-sided mark: total_drift is the bid/ask half-spread; no drift + // threshold — a wide spread self-resolves through each request's own + // min-out limit (see `FlushMark`). let vault_id = vault.id(); - let mark = lp_book::new_flush_mark(pool_nav, total_supply); + let mark = lp_book::new_flush_mark( + pool_nav + total_drift, + pool_nav.saturating_sub(total_drift), + total_supply, + ); let drain_summary = vault .lp .drain( @@ -334,13 +395,14 @@ public fun finish_flush( withdraw_budget, ctx, ); - config.end_valuation(); vault_events::emit_flush_executed( vault_id, ctx.epoch(), pool_nav, total_supply, - total_nav, + total_free_cash, + total_liability, + total_drift, market_count, idle_balance_before, drain_summary.supplies_filled(), @@ -410,8 +472,8 @@ public fun unstake_deep( /// settled-market sweep (deactivate, return all free cash, materialize profit). /// Mint asserts backing but never pulls pool cash, so this is what makes a market /// mintable. The market must already be registered to this vault -/// (`registry::create_and_share_expiry_market`). Blocked while a full-pool valuation is in -/// progress. +/// (`registry::create_and_share_expiry_market`). Cash moves need no valuation-mark +/// maintenance: the flush reads free cash live. public fun rebalance_expiry_cash( vault: &mut PoolVault, market: &mut ExpiryMarket, @@ -421,7 +483,6 @@ public fun rebalance_expiry_cash( clock: &Clock, ) { config.assert_version(); - config.assert_not_valuation_in_progress(); let expiry_market_id = market.id(); vault.expiry_accounting.assert_registered_expiry(expiry_market_id); vault.settle_or_rebalance_expiry(market, config, propbook_registry, pyth, clock); @@ -441,7 +502,6 @@ public fun claim_trading_loss_rebate( ctx: &mut TxContext, ) { config.assert_version(); - config.assert_not_valuation_in_progress(); wrapper.settle(root, clock); let account = wrapper.load_account_mut(auth); vault.claim_trading_loss_rebate_internal( @@ -471,7 +531,6 @@ public fun claim_trading_loss_rebate_permissionless( ctx: &mut TxContext, ) { config.assert_version(); - config.assert_not_valuation_in_progress(); wrapper.settle(root, clock); let auth = predict_account::generate_auth_as_app(account_registry); let account = wrapper.load_account_mut(auth); @@ -496,7 +555,6 @@ public fun sponsor_fee_incentives( ctx: &TxContext, ) { config.assert_version(); - config.assert_not_valuation_in_progress(); let amount = payment.value(); assert!( amount >= constants::min_fee_incentive_sponsorship!(), @@ -552,7 +610,6 @@ public fun request_supply( ctx: &mut TxContext, ): u64 { config.assert_version(); - config.assert_not_valuation_in_progress(); assert!(vault.lp.total_supply() > 0, ENotBootstrapped); wrapper.settle(root, clock); let account = wrapper.load_account_mut(auth); @@ -590,7 +647,6 @@ public fun request_withdraw( ctx: &mut TxContext, ): u64 { config.assert_version(); - config.assert_not_valuation_in_progress(); assert!(vault.lp.total_supply() > 0, ENotBootstrapped); wrapper.settle(root, clock); let account = wrapper.load_account_mut(auth); @@ -623,7 +679,6 @@ public fun cancel_supply_request( ctx: &mut TxContext, ) { config.assert_version(); - config.assert_not_valuation_in_progress(); let vault_id = vault.id(); wrapper.settle(root, clock); let account = wrapper.load_account_mut(auth); @@ -654,7 +709,6 @@ public fun cancel_withdraw_request( ctx: &mut TxContext, ) { config.assert_version(); - config.assert_not_valuation_in_progress(); let vault_id = vault.id(); wrapper.settle(root, clock); let account = wrapper.load_account_mut(auth); @@ -740,9 +794,11 @@ fun claim_trading_loss_rebate_internal( /// LP-attributable DUSDC pool value used to price PLP supply/withdraw. /// -/// `gross = idle_balance + active_expiry_value`. NAV prices the protocol's -/// not-yet-materialized profit share before terminal materialization and excludes -/// it from LP value: `exclusion = share * max(0, (credits + active) - debits)` +/// Assets and liabilities arrive as raw per-market sums (no per-market netting or +/// floor — see `PoolValuation`): `gross = idle_balance + Σ free_cash`, owing +/// `Σ liability`. NAV prices the protocol's not-yet-materialized profit share +/// before terminal materialization and excludes it from LP value: +/// `exclusion = share * max(0, (credits + Σ free_cash) - (debits + Σ liability))` /// (live cash returns update credits, but reserve custody waits for terminal /// profit). A cut already materialized but not yet physically moved (idle was /// deployed elsewhere) has left that debit-basis exclusion, so the carried @@ -751,25 +807,30 @@ fun claim_trading_loss_rebate_internal( fun lp_pool_value( vault: &PoolVault, protocol_reserve_profit_share: u64, - active_expiry_value: u64, + total_free_cash: u64, + total_liability: u64, ): u64 { let idle_balance = vault.expiry_accounting.idle_balance(); let profit_basis_credits = vault.expiry_accounting.profit_basis_credits(); let profit_basis_debits = vault.expiry_accounting.profit_basis_debits(); let pending_protocol_profit = vault.expiry_accounting.pending_protocol_profit(); - let gross_pool_value = idle_balance + active_expiry_value; - let aggregate_credits = profit_basis_credits + active_expiry_value; + let gross_pool_value = idle_balance + total_free_cash; + let aggregate_credits = profit_basis_credits + total_free_cash; + let aggregate_debits = profit_basis_debits + total_liability; let exclusion = math::mul( - aggregate_credits.saturating_sub(profit_basis_debits), + aggregate_credits.saturating_sub(aggregate_debits), protocol_reserve_profit_share, ); - // The realized `credits - debits` term is sticky: it does not shrink when LPs - // withdraw idle cash, so when an active mark they withdrew against later - // collapses, the held-out total (`exclusion + pending_protocol_profit`) can - // exceed gross. LP value can never be negative, so floor it at 0 to keep the - // subtraction from underflow-aborting. A 0/dust pool NAV makes non-executable - // LP queue heads refund inside `lp_book::drain`, rather than aborting the flush. - gross_pool_value.saturating_sub(exclusion + pending_protocol_profit) + // THE single policy floor for pool NAV — per-market values are deliberately + // never floored (an underwater market at backing lambda < 1 is a legitimate + // transient that must net against the rest of the pool). Two ways the + // subtraction can exceed gross: an aggregate-underwater book, and the sticky + // realized `credits - debits` term (it does not shrink when LPs withdraw idle + // cash, so a later mark collapse can push the held-out total past gross). LP + // value can never be negative, so floor at 0; a 0/dust pool NAV makes + // non-executable LP queue heads refund inside `lp_book::drain`, rather than + // aborting the flush. + gross_pool_value.saturating_sub(total_liability + exclusion + pending_protocol_profit) } /// Shared settle-or-rebalance dispatch: passively settle the market off Propbook's @@ -974,21 +1035,6 @@ fun materialize_expiry_profit( ); } -/// Engage the valuation lock and snapshot the active expiry set for the single -/// flush entrypoint, `start_pool_valuation` (lifecycle-cap-gated; the root-AdminCap -/// flush path was removed — admin's break-glass is minting itself a lifecycle cap). -/// Gated on a bootstrapped pool so the flush mark always has nonzero PLP supply. -fun start_pool_valuation_internal(config: &mut ProtocolConfig, vault: &PoolVault): PoolValuation { - assert!(vault.lp.total_supply() > 0, ENotBootstrapped); - config.begin_valuation(); - PoolValuation { - pool_vault_id: vault.id(), - expected_expiry_markets: vault.expiry_accounting.active_expiry_markets(), - valued_expiry_markets: vector[], - total_nav: 0, - } -} - /// Abort unless this valuation belongs to `vault`. fun assert_pool_vault(valuation: &PoolValuation, vault: &PoolVault) { assert!(valuation.pool_vault_id == vault.id(), EWrongPoolVault); @@ -1003,8 +1049,8 @@ fun assert_expiry_ready_to_value(valuation: &PoolValuation, expiry_market_id: ID ); } -/// The exactly-once completeness proof: the valued set must equal the snapshot -/// (a missed market means a wrong pool NAV). `value_expiry` already rejects +/// The exactly-once completeness proof: the counted set must equal the snapshot +/// (a missed market means a wrong pool NAV). `collect_expiry_nav` already rejects /// non-snapshot and duplicate ids, so equal lengths plus full coverage suffice. fun assert_all_expected_valued(expected: &vector, valued: &vector) { assert!(valued.length() == expected.length(), EMissingExpiryValuation); diff --git a/packages/predict/sources/plp/pool_accounting.move b/packages/predict/sources/plp/pool_accounting.move index f61b64308..a78b6fb56 100644 --- a/packages/predict/sources/plp/pool_accounting.move +++ b/packages/predict/sources/plp/pool_accounting.move @@ -38,6 +38,10 @@ public struct Ledger has store { /// physically moved to the reserve because idle was deployed in other active /// markets at materialization. Excluded from LP value until drained. pending_protocol_profit: u64, + /// Bumped on every internal cash move (idle<->expiry transfers and + /// protocol-profit draws from idle). Snapshotted and re-checked by the + /// flush potato (`PoolValuation.cash_revision`). + cash_revision: u64, } /// Active valuation entry with the expiry timestamp needed to classify live NAV cost. @@ -73,6 +77,7 @@ public(package) fun new(ctx: &mut TxContext): Ledger { profit_basis_credits: 0, net_losses_to_fill: 0, pending_protocol_profit: 0, + cash_revision: 0, } } @@ -80,6 +85,10 @@ public(package) fun idle_balance(ledger: &Ledger): u64 { ledger.idle_balance.value() } +public(package) fun cash_revision(ledger: &Ledger): u64 { + ledger.cash_revision +} + /// Return the expiry market IDs still contributing active pool valuation/risk. public(package) fun active_expiry_markets(ledger: &Ledger): vector { ledger.active_expiry_markets.map_ref!(|m| m.expiry_market_id) @@ -183,6 +192,7 @@ public(package) fun send_expiry_cash( ): Balance { if (amount == 0) return balance::zero(); ledger.record_sent_to_expiry(expiry_market_id, amount); + ledger.cash_revision = ledger.cash_revision + 1; ledger.idle_balance.split(amount) } @@ -216,6 +226,7 @@ public(package) fun receive_expiry_cash( }; ledger.idle_balance.join(cash); ledger.record_received_from_expiry(expiry_market_id, amount); + ledger.cash_revision = ledger.cash_revision + 1; amount } @@ -260,6 +271,7 @@ public(package) fun materialize_expiry_profit(ledger: &mut Ledger, expiry_market public(package) fun realize_pending_protocol_profit(ledger: &mut Ledger): Balance { let draw = ledger.pending_protocol_profit.min(ledger.idle_balance.value()); ledger.pending_protocol_profit = ledger.pending_protocol_profit - draw; + if (draw > 0) ledger.cash_revision = ledger.cash_revision + 1; ledger.idle_balance.split(draw) } diff --git a/packages/predict/sources/pricing/pricing.move b/packages/predict/sources/pricing/pricing.move index 453654a20..5c931209e 100644 --- a/packages/predict/sources/pricing/pricing.move +++ b/packages/predict/sources/pricing/pricing.move @@ -73,6 +73,28 @@ macro fun max_pricing_spot(): u64 { std::u64::max_value!() / 100 } macro fun min_svi_sigma(): u64 { 1_000_000 } macro fun max_svi_input(): u64 { 100 * math::float_scaling!() } +// Drift-envelope constants. A price never moves more than the normal curve's +// max slope times its d2 move; 0.4 rounds 1/sqrt(2*pi) up so the fixed-point +// round-down stays a bound. +macro fun drift_phi_max(): u64 { 400_000_000 } +// The normal density's own slope never exceeds phi(1) ~ 0.242; 0.25 rounds up. +macro fun drift_phi_prime_max(): u64 { 250_000_000 } +// Tail allowance outside the banded strike region: 2*N(-4) ~ 6.3e-5 of face +// from the pinned digitals, plus the skew-correction tail held under +// 2 * drift_ctail_target by the dynamic band threshold, plus fixed-point dust. +macro fun drift_tail_pad(): u64 { 150_000 } +// Per-snapshot skew-correction tail target: the band widens until +// phi(D) * (max wing slope / (2 * variance floor)) <= this. +macro fun drift_ctail_target(): u64 { 25_000 } +macro fun drift_band_d(): u64 { 4 * math::float_scaling!() } +// No real strike ladder sits e^100 away from the forward; a wider computed +// band means degenerate params, so the envelope fails closed instead. +macro fun drift_band_cap(): u64 { 100 * math::float_scaling!() } +// Variance floors below 1e-4 mean sub-seconds-to-expiry or a degenerate +// surface: marks there are unusable at any tolerance, so fail closed early +// (this also keeps every division below inside u64/u128 headroom). +macro fun drift_min_floor(): u64 { 100_000 } + // === Public Functions === /// Return the current UP tail price for one strike. Public read for @@ -92,7 +114,160 @@ public(package) fun expiry_market_id(pricer: &Pricer): ID { pricer.expiry_market_id } -// === Public-Package Functions === +/// Return the forward this pricer snapshotted (stored as a drift anchor). +public(package) fun forward(pricer: &Pricer): u64 { + pricer.forward +} + +/// Return a copy of the SVI params this pricer snapshotted (stored as a drift anchor). +public(package) fun svi_params(pricer: &Pricer): SVIParams { + pricer.svi +} + +/// Rebuild a `Pricer` from a valuation mark's stored anchors. Deliberately +/// bypasses `load_live_pricer`'s feed-freshness and binding guards: the +/// anchors were validated by the refresh that stored them, and this pricer +/// exists to value orders AT that stored snapshot, never at live state. +public(package) fun from_anchors(expiry_market_id: ID, forward: u64, svi: SVIParams): Pricer { + Pricer { expiry_market_id, forward, svi } +} + +/// Worst-case repricing between two oracle snapshots: an upper bound on how far +/// ANY contract's fair price can have moved between the anchor oracle state and +/// the live one, as a fraction of full payout in FLOAT_SCALING (capped at 1.0). +/// +/// This is a bound by construction, not a sample: an SVI surface IS five +/// numbers plus the forward, and every term below charges one of them, so a +/// surface change large enough to move some price somewhere must show up in at +/// least one charged term — there is no strike "between probes" to hide in. +/// The chain: a contract's price can never move more than ~0.4 x its move in +/// d2 (the normal curve's maximum slope — a property of the pricing function, +/// not of the oracle); the worst d2 move over all strikes is then bounded +/// term-by-term — the forward shift scaled by the surface's variance floor, +/// and the surface reshape (each SVI parameter's delta, Lipschitz-bounded over +/// the strike band where prices are not pinned to 0/1). Every term is zero +/// when nothing moved, and degenerate inputs (zero variance floor, wing slope +/// at/above 1, an absurd strike band) return full face — fail-closed, the mark +/// just needs a re-refresh. +/// +/// Deliberately conservative: the terms stack worst cases that cannot all +/// happen at the same strike, so benign moves are overstated (costing +/// re-refreshes, never understating drift). The implemented price also +/// subtracts a skew correction `phi(d2) * w'(k) / (2 sqrt(w))`; its own drift +/// is charged by the three skew legs below (the price clamp is 1-Lipschitz, so +/// `|dP| <= |dN(d2)| + |d correction|`), and the band threshold widens +/// dynamically until the correction's tails die under the tail allowance — +/// without that, deep-wing corrections can pull prices off their 0/1 pins (the +/// P-11 mechanism) outside a fixed band. Residual: fixed-point round-down in +/// the term arithmetic (absorbed by the tail pad; the envelope-validation +/// measurements adversarially attack the whole bound). +public(package) fun drift_envelope( + pricer: &Pricer, + anchor_forward: u64, + anchor_svi: &SVIParams, +): u64 { + // Identical snapshots price identically — exactly zero drift, no tail pad + // (the pad covers tails between DIFFERENT snapshots). This also lets an + // atomic refresh-and-flush PTB reproduce the exact single-mark behavior. + if (anchor_forward == pricer.forward && *anchor_svi == pricer.svi) return 0; + + let full = math::float_scaling!(); + let scale = full as u128; + let s0 = sqrt_min_total_variance(anchor_svi); + let s1 = sqrt_min_total_variance(&pricer.svi); + let s_lo = s0.min(s1); + if (s_lo < drift_min_floor!() || anchor_forward == 0) return full; + let s_product = math::mul(s0, s1); + if (s_product == 0) return full; + let sigma_lo = anchor_svi.sigma().min(pricer.svi.sigma()); + if (sigma_lo == 0) return full; + + // Forward leg: a forward move shifts every strike's log-moneyness by + // |ln(F1/F0)|, and d2 divides that by at least the variance floor. Guard + // the ratio in plain integer division first: a ratio this size is + // full-face drift regardless, and `math::div` would overflow-abort near + // u64::MAX rather than fail closed. + if (pricer.forward / anchor_forward >= 100) return full; + let forward_ratio = math::div(pricer.forward, anchor_forward); + if (forward_ratio == 0) return full; + let delta_forward = math::ln(forward_ratio).magnitude(); + + // Band threshold: |d2| = 4 pins the digitals, but the skew correction's + // tail dies only once phi(D) has decayed past the correction's maximum + // height r_max = max wing slope / (2 * variance floor) — widen D until + // phi(D) * r_max fits the per-snapshot tail target. + let slope0_max = math::mul(anchor_svi.b(), full + anchor_svi.rho().magnitude()); + let slope1_max = math::mul(pricer.svi.b(), full + pricer.svi.rho().magnitude()); + let r0 = math::div(slope0_max, 2 * s0); + let r1 = math::div(slope1_max, 2 * s1); + let r_max = r0.max(r1); + let mut d_threshold = drift_band_d!(); + if (r_max > drift_ctail_target!()) { + let decay_needed = math::div(math::div(drift_ctail_target!(), r_max), drift_phi_max!()); + if (decay_needed == 0) return full; + let d_ctail = math::sqrt(2 * math::ln(decay_needed).magnitude(), full); + d_threshold = d_threshold.max(d_ctail); + }; + + // Strike band where neither snapshot prices any strike away from its 0/1 + // pin by more than the tail allowance. Degenerate/absurd bands fail closed. + let band0 = unpinned_band(anchor_svi, d_threshold); + let band1 = unpinned_band(&pricer.svi, d_threshold); + if (band0.is_none() || band1.is_none()) return full; + let k_band = band0.destroy_some().max(band1.destroy_some()); + if (k_band > drift_band_cap!()) return full; + + // Surface leg: the worst total-variance gap between the snapshots over the + // band, one Lipschitz charge per SVI parameter delta (plus the band shift + // the forward move causes), converted to a sqrt-variance gap and then to a + // d2 move. + let m1_abs = pricer.svi.m().magnitude(); + let g1_max = 2 * (k_band + m1_abs) + pricer.svi.sigma(); + let delta_rho = pricer.svi.rho().sub(&anchor_svi.rho()).magnitude(); + let delta_m = pricer.svi.m().sub(&anchor_svi.m()).magnitude(); + let delta_sigma = pricer.svi.sigma().diff(anchor_svi.sigma()); + let delta_a = pricer.svi.a().diff(anchor_svi.a()); + let delta_b = pricer.svi.b().diff(anchor_svi.b()); + let sup_wing_gap = + math::mul(delta_rho, k_band + m1_abs) + math::mul(anchor_svi.rho().magnitude(), delta_m) + + delta_m + delta_sigma; + let sup_delta_w = + delta_a + math::mul(delta_b, g1_max) + math::mul(anchor_svi.b(), sup_wing_gap) + + math::mul(slope1_max, delta_forward); + let sup_delta_sqrt_w = + ((sup_delta_w as u128) * scale / ((s0 + s1) as u128)).min(100 * scale) as u64; + + // Everything below assembles in u128 with a single cap at full face: + // fail-closed saturation, never an arithmetic abort in the flush path. + let half = (full / 2) as u128; + let moneyness_ratio = (k_band as u128) * scale / (s_product as u128); + let d2_bound = + (delta_forward as u128) * scale / (s1 as u128) + + (sup_delta_sqrt_w as u128) * (moneyness_ratio + half) / scale; + let n_leg = (drift_phi_max!() as u128) * d2_bound / scale; + if (n_leg >= scale) return full; + + // Skew legs. The correction is phi(d2) * R(k) with R = w' / (2 sqrt(w)): + // charge phi_max times R's change (split into the w' change and the + // sqrt(w) change), plus R's height times the density's own move (the + // density's slope never exceeds ~0.25, and d2's move is already bounded). + let delta_u = math::div(delta_m + delta_sigma, sigma_lo); + let sup_delta_w_prime = + 2 * delta_b + math::mul(anchor_svi.b(), delta_rho + delta_u) + + math::mul(pricer.svi.b(), math::div(delta_forward, pricer.svi.sigma())); + let skew_w_prime_leg = + (drift_phi_max!() as u128) * (sup_delta_w_prime as u128) / ((2 * s_lo) as u128); + let skew_sqrt_w_leg = + (drift_phi_max!() as u128) * ((r_max as u128) * (sup_delta_sqrt_w as u128) / (s_lo as u128)) + / scale; + let skew_density_leg = + (drift_phi_prime_max!() as u128) * ((r0 as u128) * d2_bound / scale) / scale; + + let total = + n_leg + skew_w_prime_leg + skew_sqrt_w_leg + skew_density_leg + + (drift_tail_pad!() as u128); + total.min(scale) as u64 +} /// Validate the current live pricing boundary and snapshot oracle inputs for /// one market's repeated quote calculations. @@ -176,6 +351,63 @@ fun cached_up_price(memo: &PriceMemo, tick: u64): u64 { abort ETickNotInPriceMemo } +/// The size of one standard deviation of the price move this surface still +/// expects before expiry, at the strike where that expectation is smallest — +/// `sqrt(a + b * sigma * sqrt(1 - rho^2))`, the global minimum of SVI total +/// variance over strikes. The drift envelope's forward leg divides by it. +fun sqrt_min_total_variance(svi: &SVIParams): u64 { + let rho_squared = svi.rho().mul_scaled(&svi.rho()).magnitude(); + // |rho| <= 1 inside the pricing-safe envelope; saturate the complement against + // fixed-point rounding dust at the |rho| = 1 corner. + let one_minus_rho_squared = math::float_scaling!().saturating_sub(rho_squared); + let root = math::sqrt(one_minus_rho_squared, math::float_scaling!()); + let min_total_var = svi.a() + math::mul(svi.b(), math::mul(svi.sigma(), root)); + math::sqrt(min_total_var, math::float_scaling!()) +} + +/// Log-moneyness band `K` outside which this surface pins every price within +/// the tail pad of 0 or 1 (|d2| > D on both tails). Solves +/// `K = D * sqrt(W) + W / 2` against the surface's own linear overshoot +/// `W = A + B * K` (with `A = a + b * (2|m| + sigma)`, `B = 2b`, from +/// `w(k) <= A + B|k|`), so the band covers every strike the surface can price +/// away from the pins. `None` when the surface is too degenerate to band +/// (wing slope at/above ~1, or a band past the cap) — the caller fails closed. +fun unpinned_band(svi: &SVIParams, d: u64): Option { + let one = math::float_scaling!(); + let cap = drift_band_cap!(); + let a_overshoot = svi.a() + math::mul(svi.b(), 2 * svi.m().magnitude() + svi.sigma()); + let b_slope = svi.b(); + // Near/above slope 1 the quadratic degenerates (denominator -> 0); no real + // surface is close (observed b <= ~0.03), so fail closed rather than solve. + if (b_slope > 999_000_000) return option::none(); + + if (b_slope < 1_000) { + // Slope negligible: W ~ A. The 10% headroom covers the ignored slope. + let k = math::mul(d, math::sqrt(a_overshoot, one)) + a_overshoot / 2; + let k = k + k / 10; + if (k > cap) return option::none(); + return option::some(k) + }; + + let b_coeff = 2 * b_slope; + let bd = math::mul(b_coeff, d); + let discriminant = math::mul(bd, bd) + 4 * math::mul(a_overshoot, one - b_slope); + let sqrt_w = math::div(bd + math::sqrt(discriminant, one), 2 * (one - b_slope)); + let w = math::mul(sqrt_w, sqrt_w); + let w_minus_a = w.saturating_sub(a_overshoot); + // With a real slope the true W strictly exceeds A; a computed zero means + // the round-down chain swallowed the band (ill-conditioned at low + // variance — reviewed counterexample understated drift ~13.6x). No tight + // bound exists here, so fail closed: the caller charges full face and the + // market prices at its worst case until a refresh re-anchors it. + if (w_minus_a == 0) return option::none(); + // K = (W - A) / B; refuse before dividing when K would exceed the cap. + if (w_minus_a > math::mul(b_coeff, cap)) return option::none(); + let k = math::div(w_minus_a, b_coeff); + if (k > cap) return option::none(); + option::some(k) +} + fun assert_current_oracles( propbook_registry: &OracleRegistry, propbook_underlying_id: u32, @@ -397,8 +629,11 @@ fun compute_nd2(svi_params: &SVIParams, forward: u64, strike: u64): u64 { let nd2 = math::normal_cdf(&d2); if (w_prime.is_zero()) return nd2; - let correction_magnitude = - math::mul_div_down(math::normal_pdf(&d2), w_prime.magnitude(), 2 * sqrt_var); + let correction_magnitude = math::mul_div_down( + math::normal_pdf(&d2), + w_prime.magnitude(), + 2 * sqrt_var, + ); let correction = i64::from_parts(correction_magnitude, w_prime.is_negative()); let adjusted = i64::from_u64(nd2).sub(&correction); if (adjusted.is_negative()) return 0; diff --git a/packages/predict/sources/registry/registry.move b/packages/predict/sources/registry/registry.move index cc01f2f5a..87383abed 100644 --- a/packages/predict/sources/registry/registry.move +++ b/packages/predict/sources/registry/registry.move @@ -235,7 +235,6 @@ public fun create_and_share_expiry_market( config.assert_version(); registry.assert_valid_lifecycle_cap(lifecycle_cap); config.assert_trading_allowed(); - config.assert_not_valuation_in_progress(); let deployable = registry .market_manager .next_deployable_market(propbook_registry, propbook_underlying_id, cadence_id, clock); diff --git a/packages/predict/sources/strike_exposure/index/strike_payout_tree.move b/packages/predict/sources/strike_exposure/index/strike_payout_tree.move index 8e4bd667f..d86dffe12 100644 --- a/packages/predict/sources/strike_exposure/index/strike_payout_tree.move +++ b/packages/predict/sources/strike_exposure/index/strike_payout_tree.move @@ -59,6 +59,19 @@ public struct PayoutNode has copy, drop, store { summary: PayoutSummary, } +/// Return the total quantity of all live orders in the tree. Every order +/// contributes its quantity to exactly one start side (`base` for `-inf` +/// lower boundaries, a node otherwise), so the root summary's start total plus +/// the base is the whole book's face outstanding — an O(1) read of state the +/// treap already maintains. +public(package) fun total_quantity(tree: &StrikePayoutTree): u64 { + let mut total = tree.base.quantity; + if (tree.root.is_some()) { + total = total + tree.nodes[*tree.root.borrow()].summary.total_start.quantity; + }; + total +} + /// Return `(max_net_payout, total_net_payout)` for pre-settlement reserve math. public(package) fun net_payout_reserve_terms(tree: &StrikePayoutTree): (u64, u64) { let mut max_net_payout = net_payout(tree.base); diff --git a/packages/predict/sources/strike_exposure/strike_exposure.move b/packages/predict/sources/strike_exposure/strike_exposure.move index 787f83ff1..6855e435e 100644 --- a/packages/predict/sources/strike_exposure/strike_exposure.move +++ b/packages/predict/sources/strike_exposure/strike_exposure.move @@ -21,7 +21,8 @@ use deepbook_predict::{ pricing::{Self, Pricer}, range_codec, strike_exposure_config::StrikeExposureConfig, - strike_payout_tree::{Self, StrikePayoutTree} + strike_payout_tree::{Self, StrikePayoutTree}, + valuation_mark::{Self, ValuationMark} }; use fixed_math::math; use sui::clock::Clock; @@ -31,6 +32,7 @@ const EInvalidAdmissionTick: u64 = 1; const EInvalidReferenceTick: u64 = 2; const EReferenceTickAlreadySet: u64 = 3; const ETermsExposureMismatch: u64 = 4; +const EValuationMarkMissing: u64 = 5; /// Exposure lifecycle state for one expiry market. public struct StrikeExposure has store { @@ -56,6 +58,10 @@ public struct StrikeExposure has store { liquidation: LiquidationBook, /// Sparse payout tree for live cash backing and settled liability. payout: StrikePayoutTree, + /// Stored valuation mark the pool flush reads (`None` until first refresh). + /// Owned here so every book mutation writes its anchor-priced value + /// through in the same module — a trade path cannot forget the mark. + valuation_mark: Option, } /// Pure mint terms for one prospective live mint: the priced tick range, @@ -131,8 +137,12 @@ public(package) fun payout_liability(exposure: &StrikeExposure): u64 { /// Value this book's exact live liability for one live price snapshot: /// `linear - correction`, where `linear = Σ_orders qty·P` is the full payout-tree /// walk and `correction = Σ_leveraged min(qty·P, floor_shares)` is the static-floor -/// scan over the active leveraged set. The per-order floor cap makes a knocked-out -/// leveraged order net to zero, so no liquidation pass is needed for an exact mark. +/// scan over the active leveraged set. The per-order floor cap zeroes any order +/// priced at or below its floor, so the walk needs no liquidation pass to stay +/// exact — but note a liquidatable-yet-unliquidated order between `floor` and +/// `floor / ltv` still carries `gross - floor` of value here (the P-10 band), and +/// actually liquidating it removes that value (which is why liquidation writes its +/// removal through to the stored valuation mark). /// `correction <= linear` for any mint-admitted book (each leveraged order's `min` /// is capped at its own linear contribution), so the saturating_sub floors only the /// bounded valuation ulp dust the linear walk can carry, rather than aborting. A @@ -149,6 +159,32 @@ public(package) fun exact_live_liability(exposure: &StrikeExposure, pricer: &Pri linear.saturating_sub(correction) } +/// Return the stored mark's current liability: the refresh walk's number plus +/// anchor-priced trade write-through since. Never walks the payout tree — the +/// walk ran in the refresh that stored the mark, so this read loads no +/// per-order objects. Deliberately unclamped against cash: a market can +/// legitimately owe more than it holds (a backing lambda below 1 admits +/// transient shortfalls backstopped by pool rebalancing); `plp::lp_pool_value` +/// owns the single pool-level floor, and `plp` judges freshness. +public(package) fun marked_liability(exposure: &StrikeExposure): u64 { + exposure.mark().liability() +} + +/// Return the landing time of the refresh that computed the stored mark. +public(package) fun mark_computed_at_ms(exposure: &StrikeExposure): u64 { + exposure.mark().computed_at_ms() +} + +/// The stored mark's worst-case oracle drift against the live inputs in +/// `pricer`, in DUSDC base units: the endpoint envelope +/// (`valuation_mark::drift`, a fraction of full payout) times the book's live +/// face quantity — per-order value is quantity-Lipschitz in price, so this +/// bounds how far this book's liability can have drifted. `plp` sums it +/// across markets into the flush mark's bid/ask half-spread. +public(package) fun mark_drift(exposure: &StrikeExposure, pricer: &Pricer): u64 { + math::mul(exposure.payout.total_quantity(), exposure.mark().drift(pricer)) +} + /// Return the liquidation LTV snapshotted for this exposure book. public(package) fun liquidation_ltv(exposure: &StrikeExposure): u64 { exposure.config.liquidation_ltv() @@ -236,6 +272,7 @@ public(package) fun new( settled_liability_materialized: false, liquidation: liquidation_book::new(ctx), payout: strike_payout_tree::new(ctx), + valuation_mark: option::none(), } } @@ -317,6 +354,7 @@ public(package) fun allocate_mint_order(exposure: &mut StrikeExposure, terms: Mi exposure.liquidation.insert_order(&allocated_order); exposure.payout.insert_range(lower_tick, higher_tick, quantity, floor_shares); + exposure.mark_order_added(&allocated_order); allocated_order } @@ -353,11 +391,7 @@ public(package) fun quote_mint_entry_probability( /// /// Already-liquidated and currently-liquidatable orders have zero holder value; /// otherwise this returns the order's current range value net of its static floor. -public(package) fun order_value( - exposure: &StrikeExposure, - pricer: &Pricer, - order: &Order, -): u64 { +public(package) fun order_value(exposure: &StrikeExposure, pricer: &Pricer, order: &Order): u64 { if (exposure.is_liquidated_order(order)) return 0; let gross_value = exposure.gross_order_value(pricer, order); @@ -405,6 +439,9 @@ public(package) fun close_and_quote_live_order( remove_floor_shares, ); exposure.liquidation.remove_order(order); + // Anchor-priced write-through: remove the original order and (below) + // re-add any partial-close remainder, both at the mark's stored anchors. + exposure.mark_order_removed(order); let range_probability = pricer.range_price(lower, higher); let gross_redeem_amount = math::mul(range_probability, close_quantity); @@ -422,6 +459,7 @@ public(package) fun close_and_quote_live_order( ); exposure.liquidation.insert_order(&replacement_order); exposure.next_order_sequence = exposure.next_order_sequence + 1; + exposure.mark_order_added(&replacement_order); CloseQuote { resulting_order: replacement_order, redeem_amount, range_probability } } @@ -431,36 +469,35 @@ public(package) fun clear_liquidated_order(exposure: &mut StrikeExposure, order: exposure.liquidation.clear_liquidated(order); } -/// Try to liquidate one active leveraged order using exact live pricing. +/// Try to liquidate one active leveraged order using exact live pricing; a +/// knock-out is written through to the stored valuation mark and emits an +/// `OrderLiquidated` event, and an ineligible order is a no-op. public(package) fun liquidate_live_order( exposure: &mut StrikeExposure, pricer: &Pricer, order: &Order, -): bool { - if (!exposure.liquidation.contains_active_order(order)) return false; +) { + if (!exposure.liquidation.contains_active_order(order)) return; let liquidation_ltv = exposure.config.liquidation_ltv(); - exposure.liquidate_order_if_under_floor(pricer, order, liquidation_ltv) + exposure.liquidate_order_if_under_floor(pricer, order, liquidation_ltv); } -/// Run one bounded liquidation pass using exact per-candidate pricing. +/// Run one bounded liquidation pass using exact per-candidate pricing. Each +/// knock-out is written through to the stored valuation mark; per-order +/// details are on the `OrderLiquidated` events. public(package) fun liquidate_live_orders( exposure: &mut StrikeExposure, pricer: &Pricer, budget: u64, -): u64 { +) { let candidates = exposure.liquidation.select_liquidation_candidates(budget); - if (candidates.is_empty()) return 0; + if (candidates.is_empty()) return; let liquidation_ltv = exposure.config.liquidation_ltv(); - let mut liquidated_count = 0; candidates.do!(|candidate| { let order = order::from_order_id(candidate); - let liquidated = exposure.liquidate_order_if_under_floor(pricer, &order, liquidation_ltv); - if (liquidated) { - liquidated_count = liquidated_count + 1; - }; + exposure.liquidate_order_if_under_floor(pricer, &order, liquidation_ltv); }); - liquidated_count } /// Cache terminal settled payout liability. @@ -485,16 +522,59 @@ public(package) fun materialize_settled_liability( settled_liability } -fun gross_order_value( - exposure: &StrikeExposure, +/// Recompute this book's exact live liability and store it as the valuation +/// mark the pool flush reads, replacing any prior mark. Returns the stored +/// liability. +public(package) fun refresh_valuation_mark( + exposure: &mut StrikeExposure, pricer: &Pricer, - order: &Order, + clock: &Clock, ): u64 { + let liability = exposure.exact_live_liability(pricer); + exposure.valuation_mark = option::some(valuation_mark::new(liability, pricer, clock)); + liability +} + +fun gross_order_value(exposure: &StrikeExposure, pricer: &Pricer, order: &Order): u64 { let (lower, higher) = exposure.order_boundaries(order); let range_probability = pricer.range_price(lower, higher); math::mul(range_probability, order.quantity()) } +/// Borrow the stored valuation mark; aborts before the first refresh. +fun mark(exposure: &StrikeExposure): &ValuationMark { + assert!(exposure.valuation_mark.is_some(), EValuationMarkMissing); + exposure.valuation_mark.borrow() +} + +/// Value one order exactly as the liability walk counts it: anchor-priced +/// range value net of the static floor, floored at zero (this order's +/// `walk_linear` minus `correction_value` contribution). Deliberately no +/// liquidation-state zeroing — unlike `order_value`'s holder-close policy, the +/// walk carries a liquidatable-yet-unliquidated order's `gross - floor` until +/// an actual knock-out removes it. +fun mark_order_value(exposure: &StrikeExposure, order: &Order): u64 { + let anchor_pricer = exposure.mark().anchor_pricer(exposure.expiry_market_id); + exposure.gross_order_value(&anchor_pricer, order).saturating_sub(order.floor_shares()) +} + +/// Write an added order through to the stored mark at the mark's own anchors +/// (one valuation basis — see `ValuationMark`). No-op until the first refresh +/// establishes a mark. +fun mark_order_added(exposure: &mut StrikeExposure, order: &Order) { + if (exposure.valuation_mark.is_none()) return; + let value = exposure.mark_order_value(order); + exposure.valuation_mark.borrow_mut().add_value(value); +} + +/// Counterpart of `mark_order_added` for removals (live redeems and +/// liquidations). +fun mark_order_removed(exposure: &mut StrikeExposure, order: &Order) { + if (exposure.valuation_mark.is_none()) return; + let value = exposure.mark_order_value(order); + exposure.valuation_mark.borrow_mut().remove_value(value); +} + /// Liquidate (knock out) `order` when its live value has reached the static floor: /// `qty·P <= floor_shares / liquidation_ltv`. The LTV buffer is the anti-arbitrage /// enforcement margin — knock out a hair before zero equity so a missed barrier @@ -505,13 +585,12 @@ fun liquidate_order_if_under_floor( pricer: &Pricer, order: &Order, liquidation_ltv: u64, -): bool { +) { let quantity = order.quantity(); let floor_amount = order.floor_shares(); let gross_value = exposure.gross_order_value(pricer, order); let liquidation_threshold = math::div(floor_amount, liquidation_ltv); - let can_liquidate = gross_value <= liquidation_threshold; - if (!can_liquidate) return false; + if (gross_value > liquidation_threshold) return; exposure.liquidation.mark_liquidated(order); exposure @@ -522,6 +601,7 @@ fun liquidate_order_if_under_floor( quantity, floor_amount, ); + exposure.mark_order_removed(order); order_events::emit_order_liquidated( exposure.expiry_market_id, @@ -531,8 +611,6 @@ fun liquidate_order_if_under_floor( floor_amount, liquidation_ltv, ); - - true } /// Decode an order into `(lower, higher)` raw strike boundaries for pricing and diff --git a/packages/predict/sources/valuation_mark.move b/packages/predict/sources/valuation_mark.move new file mode 100644 index 000000000..7dcbe4022 --- /dev/null +++ b/packages/predict/sources/valuation_mark.move @@ -0,0 +1,102 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Stored per-market valuation mark: a dumb measurement component. +/// +/// A mark memoizes one market's exact oracle-priced per-order liability (the +/// payout-tree walk) so the pool flush can read the market without walking any +/// tree. This module owns only the mark's data discipline; acceptance policy +/// (freshness) is `plp`'s, and order valuation is `strike_exposure`'s — the +/// mark hands back its anchors as a `Pricer` and accumulates what the owner +/// valued. +module deepbook_predict::valuation_mark; + +use deepbook_predict::pricing::{Self, Pricer}; +use propbook::block_scholes_svi_feed::SVIParams; +use sui::clock::Clock; + +/// One market's stored valuation mark. Free cash is never stored — the flush +/// reads it live, so cash moves need no mark maintenance. Every component is +/// valued at the SAME stored anchors: the walked number by the refresh, the +/// trade write-through by `strike_exposure` pricing each added/removed order +/// via `anchor_pricer` — never at an op's own oracle. One valuation basis is +/// what lets the endpoint drift envelope bound the whole current book +/// (op-priced deltas are path-dependent: an oracle round trip between refresh +/// and flush leaves endpoint drift near zero while an op priced at the peak +/// stays in the sum). +public struct ValuationMark has copy, drop, store { + /// Exact oracle-priced per-order liability the refresh walk computed at + /// `computed_at_ms`. Never mutated between refreshes. + computed_liability: u64, + /// Σ anchor-priced value of orders added since the walk. + added_at_anchor: u64, + /// Σ anchor-priced value of orders removed since the walk (live redeems + /// and liquidations). + removed_at_anchor: u64, + /// On-chain landing time of the refresh that computed this mark. + computed_at_ms: u64, + /// Forward the walk priced at (drift anchor). Raw snapshot, not derived + /// terms: `drift` derives both sides with the same code at read time, so + /// the envelope formula can change without stored state aging. + anchor_forward: u64, + /// SVI params the walk priced at (drift anchor). + anchor_svi: SVIParams, +} + +// === Public-Package Functions === + +/// The mark's current liability: the walked number plus all trade deltas +/// since, netted once here. All components share the anchor basis, but the +/// walk aggregates quantities per tree range before multiplying while +/// write-through prices per order, so netting can exceed the sum by +/// fixed-point association dust — clamp that dust rather than abort a read in +/// the mandatory flush path. +public(package) fun liability(mark: &ValuationMark): u64 { + (mark.computed_liability + mark.added_at_anchor).saturating_sub(mark.removed_at_anchor) +} + +public(package) fun computed_at_ms(mark: &ValuationMark): u64 { + mark.computed_at_ms +} + +/// Measure this mark's potential oracle drift against the live inputs in +/// `pricer`: an upper bound on how far ANY single contract's fair price can +/// have moved since the walk, as a fraction of full payout in FLOAT_SCALING +/// (`pricing::drift_envelope` between the stored anchors and the live +/// snapshot). Oracle movement only — trade deltas are already inside +/// `liability`. +public(package) fun drift(mark: &ValuationMark, pricer: &Pricer): u64 { + pricer.drift_envelope(mark.anchor_forward, &mark.anchor_svi) +} + +/// Rebuild the stored anchors as a `Pricer` so the mark's owner can value +/// write-through orders at the SAME snapshot the walk priced. +public(package) fun anchor_pricer(mark: &ValuationMark, expiry_market_id: ID): Pricer { + pricing::from_anchors(expiry_market_id, mark.anchor_forward, mark.anchor_svi) +} + +/// Snapshot a fresh mark: the just-walked liability with zeroed trade deltas, +/// anchored to the oracle inputs the walk priced at. +public(package) fun new(liability: u64, pricer: &Pricer, clock: &Clock): ValuationMark { + ValuationMark { + computed_liability: liability, + added_at_anchor: 0, + removed_at_anchor: 0, + computed_at_ms: clock.timestamp_ms(), + anchor_forward: pricer.forward(), + anchor_svi: pricer.svi_params(), + } +} + +/// Accumulate an added order's anchor-priced value (produced against +/// `anchor_pricer`). +public(package) fun add_value(mark: &mut ValuationMark, value: u64) { + mark.added_at_anchor = mark.added_at_anchor + value; +} + +/// Accumulate a removed order's anchor-priced value (produced against +/// `anchor_pricer`). Never clamps here — the dust netting happens once, in +/// `liability`. +public(package) fun remove_value(mark: &mut ValuationMark, value: u64) { + mark.removed_at_anchor = mark.removed_at_anchor + value; +} diff --git a/packages/predict/tests/helper/oracle_fixture.move b/packages/predict/tests/helper/oracle_fixture.move index 39d57b124..5421185e5 100644 --- a/packages/predict/tests/helper/oracle_fixture.move +++ b/packages/predict/tests/helper/oracle_fixture.move @@ -495,6 +495,68 @@ public fun set_bs_forward_for_testing_bundle( self.set_bs_forward_for_testing(&mut oracle.bs, source_timestamp_ms, forward); } +/// Overwrite only the BS SVI row for this fixture's expiry through the real +/// ingest path. Lets a test seed a SECOND surface snapshot: the oracle lane +/// keeps the newest source timestamp, so re-seeding requires advancing it. +public fun set_bs_svi_for_testing( + self: &mut OracleFixture, + bs: &mut BlockScholesFeed, + source_timestamp_ms: u64, + svi_a: u64, + svi_b: u64, + svi_sigma: u64, + svi_rho_magnitude: u64, + svi_rho_is_negative: bool, + svi_m_magnitude: u64, + svi_m_is_negative: bool, +) { + let bs_source_id = bs.svi().bs_source_id(); + bs + .svi_mut() + .update( + update::new_svi_update( + bs_source_id, + self.expiry, + source_timestamp_ms, + svi_a, + svi_b, + svi_sigma, + svi_rho_magnitude, + svi_rho_is_negative, + svi_m_magnitude, + svi_m_is_negative, + ), + &self.clock, + self.scenario.ctx(), + ); +} + +/// Overwrite only the BS SVI row through an oracle bundle. +public fun set_bs_svi_for_testing_bundle( + self: &mut OracleFixture, + oracle: &mut OracleBundle, + source_timestamp_ms: u64, + svi_a: u64, + svi_b: u64, + svi_sigma: u64, + svi_rho_magnitude: u64, + svi_rho_is_negative: bool, + svi_m_magnitude: u64, + svi_m_is_negative: bool, +) { + self.set_bs_svi_for_testing( + &mut oracle.bs, + source_timestamp_ms, + svi_a, + svi_b, + svi_sigma, + svi_rho_magnitude, + svi_rho_is_negative, + svi_m_magnitude, + svi_m_is_negative, + ); +} + /// Overwrite the Pyth spot directly (for staleness / pricing-source tests), keeping /// the fixture clock as the on-chain landing timestamp. public fun set_pyth( diff --git a/packages/predict/tests/pricing/drift_reference_data.move b/packages/predict/tests/pricing/drift_reference_data.move new file mode 100644 index 000000000..9355a3316 --- /dev/null +++ b/packages/predict/tests/pricing/drift_reference_data.move @@ -0,0 +1,202 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 +// +// @generated by predict-research scripts/drift_fixture_gen.py — DO NOT EDIT. +// Fidelity vectors for the drift envelope: each row's two snapshots are seeded +// through the production oracle ingest and the chain's drift_envelope/up_price +// must equal the embedded values exactly. The same vectors are committed to +// predict-research as scripts/fixtures/drift_vectors_v1.csv, where the Python +// measurement replica asserts the identical outputs — a green run on both +// sides proves the replica computes what the chain computes (M0 gate for the +// DBU-557 drift measurements). +#[test_only] +module deepbook_predict::drift_reference_data; + +/// One fidelity vector: two oracle snapshots, a probe strike, and the exact +/// expected chain outputs. `has_price` is false for degenerate surfaces whose +/// up_price aborts (EZeroVariance) — those rows pin only the envelope. +public struct DriftVector has copy, drop { + spot0: u64, + bs_forward0: u64, + a0: u64, + b0: u64, + sigma0: u64, + rho0_mag: u64, + rho0_neg: bool, + m0_mag: u64, + m0_neg: bool, + spot1: u64, + bs_forward1: u64, + a1: u64, + b1: u64, + sigma1: u64, + rho1_mag: u64, + rho1_neg: bool, + m1_mag: u64, + m1_neg: bool, + strike: u64, + has_price: bool, + forward0: u64, + forward1: u64, + envelope: u64, + up0: u64, + up1: u64, +} + +public fun vectors(): vector { + vector[ + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 206963, b0: 33862461, sigma0: 75865281, rho0_mag: 731456866, rho0_neg: true, m0_mag: 2694344, m0_neg: false, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 206963, b1: 33862461, sigma1: 75865281, rho1_mag: 731456866, rho1_neg: true, m1_mag: 2694344, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 0, up0: 586463895, up1: 586463895 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107001000000000, a0: 206963, b0: 33862461, sigma0: 75865281, rho0_mag: 731456866, rho0_neg: true, m0_mag: 2694344, m0_neg: false, spot1: 107000000000000, bs_forward1: 107001000000000, a1: 206963, b1: 33862461, sigma1: 75865281, rho1_mag: 731456866, rho1_neg: true, m1_mag: 2694344, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000999915000, forward1: 107000999915000, envelope: 0, up0: 586545171, up1: 586545171 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 0, b0: 0, sigma0: 1000000, rho0_mag: 0, rho0_neg: false, m0_mag: 0, m0_neg: false, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 206963, b1: 33862461, sigma1: 75865281, rho1_mag: 731456866, rho1_neg: true, m1_mag: 2694344, m1_neg: false, strike: 107000000000000, has_price: false, forward0: 107000000000000, forward1: 107000000000000, envelope: 1000000000, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 206963, b0: 33862461, sigma0: 75865281, rho0_mag: 731456866, rho0_neg: true, m0_mag: 2694344, m0_neg: false, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 0, b1: 0, sigma1: 1000000, rho1_mag: 0, rho1_neg: false, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: false, forward0: 107000000000000, forward1: 107000000000000, envelope: 1000000000, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 206963, b0: 33862461, sigma0: 75865281, rho0_mag: 731456866, rho0_neg: true, m0_mag: 2694344, m0_neg: false, spot1: 10700000000000000, bs_forward1: 10700000000000000, a1: 206963, b1: 33862461, sigma1: 75865281, rho1_mag: 731456866, rho1_neg: true, m1_mag: 2694344, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 10700000000000000, envelope: 1000000000, up0: 586463895, up1: 1000000000 }, + DriftVector { spot0: 5350000000000000, bs_forward0: 5350000000000000, a0: 206963, b0: 33862461, sigma0: 75865281, rho0_mag: 731456866, rho0_neg: true, m0_mag: 2694344, m0_neg: false, spot1: 2000000000, bs_forward1: 2000000000, a1: 206963, b1: 33862461, sigma1: 75865281, rho1_mag: 731456866, rho1_neg: true, m1_mag: 2694344, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 5350000000000000, forward1: 2000000000, envelope: 1000000000, up0: 1000000000, up1: 0 }, + DriftVector { spot0: 100000000000000000, bs_forward0: 100000000000000000, a0: 206963, b0: 33862461, sigma0: 75865281, rho0_mag: 731456866, rho0_neg: true, m0_mag: 2694344, m0_neg: false, spot1: 2, bs_forward1: 2, a1: 206963, b1: 33862461, sigma1: 75865281, rho1_mag: 731456866, rho1_neg: true, m1_mag: 2694344, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 100000000000000000, forward1: 2, envelope: 1000000000, up0: 1000000000, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 0, b0: 10000000000, sigma0: 1000000, rho0_mag: 999999999, rho0_neg: true, m0_mag: 0, m0_neg: false, spot1: 107001000000000, bs_forward1: 107001000000000, a1: 1, b1: 10000000000, sigma1: 1000000, rho1_mag: 999999999, rho1_neg: true, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107001000000000, envelope: 1000000000, up0: 1000000000, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 0, b0: 30000000000, sigma0: 1000000, rho0_mag: 999999999, rho0_neg: true, m0_mag: 0, m0_neg: false, spot1: 107001000000000, bs_forward1: 107001000000000, a1: 1, b1: 30000000000, sigma1: 1000000, rho1_mag: 999999999, rho1_neg: true, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107001000000000, envelope: 1000000000, up0: 1000000000, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 0, b0: 100000000000, sigma0: 1000000, rho0_mag: 999999999, rho0_neg: true, m0_mag: 0, m0_neg: false, spot1: 107001000000000, bs_forward1: 107001000000000, a1: 1, b1: 100000000000, sigma1: 1000000, rho1_mag: 999999999, rho1_neg: true, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107001000000000, envelope: 1000000000, up0: 1000000000, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 50000000000, b0: 999500000, sigma0: 10000000, rho0_mag: 0, rho0_neg: false, m0_mag: 0, m0_neg: false, spot1: 107001000000000, bs_forward1: 107001000000000, a1: 50000000001, b1: 999500000, sigma1: 10000000, rho1_mag: 0, rho1_neg: false, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107001000000000, envelope: 1000000000, up0: 203203, up1: 203254 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 9, b0: 1000, sigma0: 3000000, rho0_mag: 0, rho0_neg: false, m0_mag: 0, m0_neg: false, spot1: 107001000000000, bs_forward1: 107001000000000, a1: 8, b1: 1000, sigma1: 3000000, rho1_mag: 0, rho1_neg: false, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107001000000000, envelope: 1000000000, up0: 499978150, up1: 535482459 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 9, b0: 1500, sigma0: 1500000, rho0_mag: 0, rho0_neg: false, m0_mag: 0, m0_neg: false, spot1: 107001000000000, bs_forward1: 107001000000000, a1: 8, b1: 1500, sigma1: 1500000, rho1_mag: 0, rho1_neg: false, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107001000000000, envelope: 1000000000, up0: 499980982, up1: 537221006 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 9, b0: 1500, sigma0: 3000000, rho0_mag: 0, rho0_neg: false, m0_mag: 0, m0_neg: false, spot1: 107001000000000, bs_forward1: 107001000000000, a1: 8, b1: 1500, sigma1: 3000000, rho1_mag: 0, rho1_neg: false, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107001000000000, envelope: 1000000000, up0: 499979007, up1: 533973664 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 9, b0: 2000, sigma0: 1000000, rho0_mag: 0, rho0_neg: false, m0_mag: 0, m0_neg: false, spot1: 107001000000000, bs_forward1: 107001000000000, a1: 8, b1: 2000, sigma1: 1000000, rho1_mag: 0, rho1_neg: false, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107001000000000, envelope: 1000000000, up0: 499980982, up1: 537238880 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 5000, b0: 50000, sigma0: 2000000, rho0_mag: 500000000, rho0_neg: true, m0_mag: 0, m0_neg: false, spot1: 321000000000000, bs_forward1: 321000000000000, a1: 5000, b1: 50000, sigma1: 2000000, rho1_mag: 500000000, rho1_neg: true, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 321000000000000, envelope: 1000000000, up0: 501757713, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 5000, b0: 50000, sigma0: 2000000, rho0_mag: 500000000, rho0_neg: true, m0_mag: 0, m0_neg: false, spot1: 109140000000000, bs_forward1: 109140000000000, a1: 5000, b1: 50000, sigma1: 2000000, rho1_mag: 500000000, rho1_neg: true, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 109140000000000, envelope: 1000000000, up0: 501757713, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 100000000000, b0: 500, sigma0: 1000000, rho0_mag: 0, rho0_neg: false, m0_mag: 0, m0_neg: false, spot1: 107001000000000, bs_forward1: 107001000000000, a1: 1000, b1: 50000000, sigma1: 1000000, rho1_mag: 950000000, rho1_neg: true, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107001000000000, envelope: 1000000000, up0: 286, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 50000000000, b0: 500000000, sigma0: 1000000, rho0_mag: 0, rho0_neg: false, m0_mag: 0, m0_neg: false, spot1: 107001000000000, bs_forward1: 107001000000000, a1: 49999999999, b1: 500000000, sigma1: 1000000, rho1_mag: 0, rho1_neg: false, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107001000000000, envelope: 1000000000, up0: 203462, up1: 203717 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 100000000000, b0: 100000000, sigma0: 1000000, rho0_mag: 0, rho0_neg: false, m0_mag: 0, m0_neg: false, spot1: 107001000000000, bs_forward1: 107001000000000, a1: 99999999999, b1: 100000000, sigma1: 1000000, rho1_mag: 0, rho1_neg: false, m1_mag: 0, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107001000000000, envelope: 1000000000, up0: 286, up1: 286 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 558, b0: 12069, sigma0: 1000000, rho0_mag: 849637021, rho0_neg: true, m0_mag: 511621, m0_neg: true, spot1: 107113238581319, bs_forward1: 107113238581319, a1: 560, b1: 12071, sigma1: 1000001, rho1_mag: 849636563, rho1_neg: true, m1_mag: 511621, m1_neg: true, strike: 105716000000000, has_price: true, forward0: 107000000000000, forward1: 107113238581319, envelope: 609264965, up0: 1000000000, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 4103, b0: 62222, sigma0: 1000000, rho0_mag: 668836139, rho0_neg: true, m0_mag: 2434727, m0_neg: true, spot1: 106862148601245, bs_forward1: 106862148601245, a1: 4104, b1: 62216, sigma1: 1000097, rho1_mag: 668871050, rho1_neg: true, m1_mag: 2434510, m1_neg: true, strike: 106743000000000, has_price: true, forward0: 107000000000000, forward1: 106862148601245, envelope: 295961769, up0: 882461526, up1: 706958183 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 273413, b0: 15232521, sigma0: 22951218, rho0_mag: 210693199, rho0_neg: true, m0_mag: 13811427, m0_neg: false, spot1: 107065330190083, bs_forward1: 107065330190083, a1: 271703, b1: 15374435, sigma1: 22943995, rho1_mag: 212226226, rho1_neg: true, m1_mag: 13916433, m1_neg: false, strike: 109247000000000, has_price: true, forward0: 107000000000000, forward1: 107065330190083, envelope: 1000000000, up0: 190986634, up1: 200340149 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 2878, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 958156, m0_neg: true, spot1: 106946808616692, bs_forward1: 106946808616692, a1: 3361, b1: 102825, sigma1: 1000000, rho1_mag: 963900696, rho1_neg: true, m1_mag: 1091885, m1_neg: true, strike: 110006000000000, has_price: true, forward0: 107000000000000, forward1: 106946808616692, envelope: 369012750, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 3301, b0: 212468, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 1400582, m0_neg: true, spot1: 107178923889270, bs_forward1: 107178923889270, a1: 3301, b1: 212469, sigma1: 1000000, rho1_mag: 939999870, rho1_neg: true, m1_mag: 1400582, m1_neg: true, strike: 107716000000000, has_price: true, forward0: 107000000000000, forward1: 107178923889270, envelope: 722797952, up0: 151684, up1: 3314590 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 1104, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 44973, m0_neg: false, spot1: 107144515187123, bs_forward1: 107144515187123, a1: 1104, b1: 99990, sigma1: 1000000, rho1_mag: 939936308, rho1_neg: true, m1_mag: 44971, m1_neg: false, strike: 104956000000000, has_price: true, forward0: 107000000000000, forward1: 107144515187123, envelope: 850129662, up0: 1000000000, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 55728, b0: 7779162, sigma0: 10177339, rho0_mag: 291291200, rho0_neg: true, m0_mag: 102783, m0_neg: false, spot1: 106886924073889, bs_forward1: 106886924073889, a1: 56153, b1: 7708980, sigma1: 10132075, rho1_mag: 289091630, rho1_neg: true, m1_mag: 101877, m1_neg: false, strike: 108626000000000, has_price: true, forward0: 107000000000000, forward1: 106886924073889, envelope: 1000000000, up0: 85079194, up1: 74350771 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 6069, b0: 115917, sigma0: 2843876, rho0_mag: 940851141, rho0_neg: true, m0_mag: 11276306, m0_neg: false, spot1: 107077921977012, bs_forward1: 107077921977012, a1: 5781, b1: 123173, sigma1: 3445575, rho1_mag: 934285662, rho1_neg: true, m1_mag: 11755781, m1_neg: false, strike: 109600000000000, has_price: true, forward0: 107000000000000, forward1: 107077921977012, envelope: 339457813, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 3238, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 2949756, m0_neg: false, spot1: 107203411212307, bs_forward1: 107203411212307, a1: 3236, b1: 99998, sigma1: 1000001, rho1_mag: 940000853, rho1_neg: true, m1_mag: 2949756, m1_neg: false, strike: 104464000000000, has_price: true, forward0: 107000000000000, forward1: 107203411212307, envelope: 581825921, up0: 1000000000, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 4739, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 1284911, m0_neg: true, spot1: 107149208044636, bs_forward1: 107149208044636, a1: 4740, b1: 99993, sigma1: 1000073, rho1_mag: 940021080, rho1_neg: true, m1_mag: 1284792, m1_neg: true, strike: 107952000000000, has_price: true, forward0: 107000000000000, forward1: 107149208044636, envelope: 333141377, up0: 26305, up1: 324785 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 305464, b0: 20690976, sigma0: 23791399, rho0_mag: 170679679, rho0_neg: true, m0_mag: 13306193, m0_neg: false, spot1: 106901910748084, bs_forward1: 106901910748084, a1: 305636, b1: 20868287, sigma1: 23858961, rho1_mag: 169507463, rho1_neg: true, m1_mag: 13394906, m1_neg: false, strike: 105930000000000, has_price: true, forward0: 107000000000000, forward1: 106901910748084, envelope: 1000000000, up0: 719012795, up1: 708895757 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 714370, b0: 20537765, sigma0: 29570036, rho0_mag: 631329040, rho0_neg: true, m0_mag: 2384674, m0_neg: false, spot1: 106859418349832, bs_forward1: 106859418349832, a1: 500081, b1: 17399200, sigma1: 28556544, rho1_mag: 532853076, rho1_neg: true, m1_mag: 2934990, m1_neg: false, strike: 108433000000000, has_price: true, forward0: 107000000000000, forward1: 106859418349832, envelope: 1000000000, up0: 376065412, up1: 325853203 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 1417, b0: 48275, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 809113, m0_neg: false, spot1: 106955280679957, bs_forward1: 106955280679957, a1: 1417, b1: 48275, sigma1: 1000000, rho1_mag: 940000668, rho1_neg: true, m1_mag: 809113, m1_neg: false, strike: 109247000000000, has_price: true, forward0: 107000000000000, forward1: 106955280679957, envelope: 173821497, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 416, b0: 13610, sigma0: 1000000, rho0_mag: 923179193, rho0_neg: true, m0_mag: 2398616, m0_neg: true, spot1: 106931690945152, bs_forward1: 106931690945152, a1: 416, b1: 13612, sigma1: 1000096, rho1_mag: 923250696, rho1_neg: true, m1_mag: 2398515, m1_neg: true, strike: 104539000000000, has_price: true, forward0: 107000000000000, forward1: 106931690945152, envelope: 437019084, up0: 1000000000, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 2254, b0: 107550, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 1668891, m0_neg: true, spot1: 107125898720136, bs_forward1: 107125898720136, a1: 2255, b1: 107137, sigma1: 1000000, rho1_mag: 949077059, rho1_neg: true, m1_mag: 1652619, m1_neg: true, strike: 108123000000000, has_price: true, forward0: 107000000000000, forward1: 107125898720136, envelope: 480616534, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 587, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 82148, m0_neg: false, spot1: 106889727226653, bs_forward1: 106889727226653, a1: 479, b1: 125455, sigma1: 1278737, rho1_mag: 853857893, rho1_neg: true, m1_mag: 71128, m1_neg: false, strike: 105502000000000, has_price: true, forward0: 107000000000000, forward1: 106889727226653, envelope: 1000000000, up0: 1000000000, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 79, b0: 21123, sigma0: 67569915, rho0_mag: 811376173, rho0_neg: true, m0_mag: 19710886, m0_neg: false, spot1: 107170580698443, bs_forward1: 107170580698443, a1: 80, b1: 21121, sigma1: 67569863, rho1_mag: 811375560, rho1_neg: true, m1_mag: 19710900, m1_neg: false, strike: 104881000000000, has_price: true, forward0: 107000000000000, forward1: 107170580698443, envelope: 766163037, up0: 1000000000, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 1840, b0: 77500, sigma0: 1000000, rho0_mag: 652336704, rho0_neg: true, m0_mag: 1408644, m0_neg: true, spot1: 107135758185467, bs_forward1: 107135758185467, a1: 1841, b1: 77503, sigma1: 1000000, rho1_mag: 652345932, rho1_neg: true, m1_mag: 1408777, m1_neg: true, strike: 106679000000000, has_price: true, forward0: 107000000000000, forward1: 107135758185467, envelope: 486408598, up0: 983489900, up1: 998198482 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 108545, b0: 7287824, sigma0: 58786474, rho0_mag: 478579309, rho0_neg: true, m0_mag: 27614318, m0_neg: false, spot1: 106952207732757, bs_forward1: 106952207732757, a1: 109543, b1: 7320648, sigma1: 58256355, rho1_mag: 481480918, rho1_neg: true, m1_mag: 27534963, m1_neg: false, strike: 108861000000000, has_price: true, forward0: 107000000000000, forward1: 106952207732757, envelope: 281136165, up0: 263468317, up1: 256829678 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 701, b0: 53125, sigma0: 1000000, rho0_mag: 481819375, rho0_neg: true, m0_mag: 391786, m0_neg: true, spot1: 107009106670317, bs_forward1: 107009106670317, a1: 807, b1: 40494, sigma1: 1262896, rho1_mag: 588888386, rho1_neg: true, m1_mag: 320426, m1_neg: true, strike: 109685000000000, has_price: true, forward0: 107000000000000, forward1: 107009106670317, envelope: 436974659, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 513048, b0: 8962514, sigma0: 23914793, rho0_mag: 652532402, rho0_neg: true, m0_mag: 794308, m0_neg: true, spot1: 107172571198171, bs_forward1: 107172571198171, a1: 513046, b1: 8962512, sigma1: 23914807, rho1_mag: 652532468, rho1_neg: true, m1_mag: 794309, m1_neg: true, strike: 108958000000000, has_price: true, forward0: 107000000000000, forward1: 107172571198171, envelope: 146710668, up0: 240371313, up1: 262080627 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 3842, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 783096, m0_neg: false, spot1: 107081054199303, bs_forward1: 107081054199303, a1: 3843, b1: 100003, sigma1: 1000080, rho1_mag: 939975415, rho1_neg: true, m1_mag: 783028, m1_neg: false, strike: 107973000000000, has_price: true, forward0: 107000000000000, forward1: 107081054199303, envelope: 205925571, up0: 2238, up1: 13000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 882194, b0: 13111553, sigma0: 47569051, rho0_mag: 832705553, rho0_neg: true, m0_mag: 12445785, m0_neg: false, spot1: 107175646754438, bs_forward1: 107175646754438, a1: 874694, b1: 13180258, sigma1: 47538466, rho1_mag: 831425974, rho1_neg: true, m1_mag: 12410579, m1_neg: false, strike: 106251000000000, has_price: true, forward0: 107000000000000, forward1: 107175646754438, envelope: 439852007, up0: 632582987, up1: 648374215 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 297799, b0: 11222024, sigma0: 29189514, rho0_mag: 134207954, rho0_neg: true, m0_mag: 19730286, m0_neg: false, spot1: 106885483513608, bs_forward1: 106885483513608, a1: 310321, b1: 9664166, sigma1: 20740132, rho1_mag: 163918783, rho1_neg: true, m1_mag: 16755193, m1_neg: false, strike: 104464000000000, has_price: true, forward0: 107000000000000, forward1: 106885483513608, envelope: 1000000000, up0: 829171792, up1: 838204963 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 9393, b0: 78272, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 7421957, m0_neg: true, spot1: 106929376499472, bs_forward1: 106929376499472, a1: 9391, b1: 78270, sigma1: 1000002, rho1_mag: 939999540, rho1_neg: true, m1_mag: 7421961, m1_neg: true, strike: 105181000000000, has_price: true, forward0: 107000000000000, forward1: 106929376499472, envelope: 101260924, up0: 999999912, up1: 999999774 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 0, b0: 30000, sigma0: 40050468, rho0_mag: 941096823, rho0_neg: true, m0_mag: 47055, m0_neg: false, spot1: 106884522924718, bs_forward1: 106884522924718, a1: 0, b1: 30002, sigma1: 40053539, rho1_mag: 941017796, rho1_neg: true, m1_mag: 47054, m1_neg: false, strike: 109589000000000, has_price: true, forward0: 107000000000000, forward1: 106884522924718, envelope: 939559276, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 7778, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 2024067, m0_neg: false, spot1: 107064505386046, bs_forward1: 107064505386046, a1: 7782, b1: 100607, sigma1: 1000000, rho1_mag: 944368432, rho1_neg: true, m1_mag: 2020877, m1_neg: false, strike: 105170000000000, has_price: true, forward0: 107000000000000, forward1: 107064505386046, envelope: 111080007, up0: 999999841, up1: 999999926 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 1908, b0: 51613, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 1940813, m0_neg: false, spot1: 107077594612659, bs_forward1: 107077594612659, a1: 1692, b1: 44476, sigma1: 1257887, rho1_mag: 907922798, rho1_neg: true, m1_mag: 2365575, m1_neg: false, strike: 105106000000000, has_price: true, forward0: 107000000000000, forward1: 107077594612659, envelope: 513400338, up0: 1000000000, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 2133, b0: 70588, sigma0: 1000000, rho0_mag: 293697176, rho0_neg: true, m0_mag: 1225154, m0_neg: true, spot1: 106873041334751, bs_forward1: 106873041334751, a1: 2132, b1: 70586, sigma1: 1000000, rho1_mag: 293696951, rho1_neg: true, m1_mag: 1225153, m1_neg: true, strike: 107224000000000, has_price: true, forward0: 107000000000000, forward1: 106873041334751, envelope: 388508766, up0: 81825644, up1: 15816380 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 154630, b0: 4324846, sigma0: 14282671, rho0_mag: 106829328, rho0_neg: true, m0_mag: 11383262, m0_neg: false, spot1: 107125618349539, bs_forward1: 107125618349539, a1: 154636, b1: 4324742, sigma1: 14282203, rho1_mag: 106820611, rho1_neg: true, m1_mag: 11382416, m1_neg: false, strike: 108262000000000, has_price: true, forward0: 107000000000000, forward1: 107125618349539, envelope: 107926618, up0: 214017874, up1: 242005211 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 4541, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 1521070, m0_neg: false, spot1: 106990352379175, bs_forward1: 106990352379175, a1: 4546, b1: 99061, sigma1: 1000000, rho1_mag: 948639134, rho1_neg: true, m1_mag: 1521258, m1_neg: false, strike: 105095000000000, has_price: true, forward0: 107000000000000, forward1: 106990352379175, envelope: 30983215, up0: 1000000000, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 318484, b0: 10087573, sigma0: 19724542, rho0_mag: 459396385, rho0_neg: true, m0_mag: 5344332, m0_neg: false, spot1: 106908667442851, bs_forward1: 106908667442851, a1: 339753, b1: 10727877, sigma1: 16352821, rho1_mag: 415737442, rho1_neg: true, m1_mag: 5708856, m1_neg: false, strike: 105352000000000, has_price: true, forward0: 107000000000000, forward1: 106908667442851, envelope: 1000000000, up0: 792486577, up1: 787375657 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 5409, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 1658142, m0_neg: false, spot1: 106848781592200, bs_forward1: 106848781592200, a1: 5411, b1: 99998, sigma1: 1000000, rho1_mag: 939999176, rho1_neg: true, m1_mag: 1658141, m1_neg: false, strike: 108765000000000, has_price: true, forward0: 107000000000000, forward1: 106848781592200, envelope: 312277578, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 8449, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 342221, m0_neg: false, spot1: 107198558691608, bs_forward1: 107198558691608, a1: 8449, b1: 99995, sigma1: 1000000, rho1_mag: 939987079, rho1_neg: true, m1_mag: 342234, m1_neg: false, strike: 105320000000000, has_price: true, forward0: 107000000000000, forward1: 107198558691608, envelope: 313010915, up0: 999998550, up1: 999999865 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 245, b0: 6251, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 1137446, m0_neg: false, spot1: 106790792278231, bs_forward1: 106790792278231, a1: 244, b1: 6278, sigma1: 1000000, rho1_mag: 933831660, rho1_neg: true, m1_mag: 1127622, m1_neg: false, strike: 107588000000000, has_price: true, forward0: 107000000000000, forward1: 106790792278231, envelope: 1000000000, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 1649, b0: 100004, sigma0: 42129114, rho0_mag: 950000000, rho0_neg: true, m0_mag: 995228, m0_neg: true, spot1: 106929209645549, bs_forward1: 106929209645549, a1: 1840, b1: 129926, sigma1: 36810625, rho1_mag: 863466989, rho1_neg: true, m1_mag: 712964, m1_neg: true, strike: 109107000000000, has_price: true, forward0: 107000000000000, forward1: 106929209645549, envelope: 1000000000, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 183727, b0: 10148460, sigma0: 19531867, rho0_mag: 82477637, rho0_neg: true, m0_mag: 9913429, m0_neg: false, spot1: 107204603525988, bs_forward1: 107204603525988, a1: 183727, b1: 10148457, sigma1: 19531852, rho1_mag: 82477653, rho1_neg: true, m1_mag: 9913426, m1_neg: false, strike: 108583000000000, has_price: true, forward0: 107000000000000, forward1: 107204603525988, envelope: 308665634, up0: 211795788, up1: 248239489 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 2035, b0: 100000, sigma0: 1452290, rho0_mag: 578321225, rho0_neg: true, m0_mag: 2001871, m0_neg: true, spot1: 106833677578891, bs_forward1: 106833677578891, a1: 2036, b1: 100009, sigma1: 1452362, rho1_mag: 578312230, rho1_neg: true, m1_mag: 2001706, m1_neg: true, strike: 109065000000000, has_price: true, forward0: 107000000000000, forward1: 106833677578891, envelope: 586523642, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 2489, b0: 100002, sigma0: 1400998, rho0_mag: 833864641, rho0_neg: true, m0_mag: 2236012, m0_neg: true, spot1: 107070013646230, bs_forward1: 107070013646230, a1: 2474, b1: 99850, sigma1: 1387199, rho1_mag: 831505684, rho1_neg: true, m1_mag: 2234238, m1_neg: true, strike: 109782000000000, has_price: true, forward0: 107000000000000, forward1: 107070013646230, envelope: 239386290, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 2847, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 455016, m0_neg: false, spot1: 107056846279615, bs_forward1: 107056846279615, a1: 2014, b1: 96249, sigma1: 1000000, rho1_mag: 1000000000, rho1_neg: true, m1_mag: 438994, m1_neg: false, strike: 109236000000000, has_price: true, forward0: 107000000000000, forward1: 107056846279615, envelope: 685576405, up0: 0, up1: 0 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 4967813, b0: 33132355, sigma0: 107214859, rho0_mag: 739442806, rho0_neg: true, m0_mag: 2419918, m0_neg: true, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 4967814, b1: 33132355, sigma1: 107214859, rho1_mag: 739442806, rho1_neg: true, m1_mag: 2419918, m1_neg: true, strike: 106893000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 150449, up0: 538030245, up1: 538030238 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 2567086, b0: 21346560, sigma0: 90603142, rho0_mag: 356387287, rho0_neg: true, m0_mag: 1230994, m0_neg: true, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 2567087, b1: 21346560, sigma1: 90603142, rho1_mag: 356387287, rho1_neg: true, m1_mag: 1230994, m1_neg: true, strike: 106721000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 150611, up0: 525664952, up1: 525664942 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 4705046, b0: 30732002, sigma0: 14622276, rho0_mag: 61103397, rho0_neg: false, m0_mag: 55536, m0_neg: false, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 4705047, b1: 30732002, sigma1: 14622276, rho1_mag: 61103397, rho1_neg: false, m1_mag: 55536, m1_neg: false, strike: 107278000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 150622, up0: 451532831, up1: 451532831 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 3526273, b0: 2367578, sigma0: 94426494, rho0_mag: 2230746, rho0_neg: false, m0_mag: 2692688, m0_neg: true, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 3526273, b1: 2367579, sigma1: 94426494, rho1_mag: 2230746, rho1_neg: false, m1_mag: 2692688, m1_neg: true, strike: 106796000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 150006, up0: 500136577, up1: 500136577 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 692982, b0: 24784509, sigma0: 72213155, rho0_mag: 698633347, rho0_neg: false, m0_mag: 2408367, m0_neg: false, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 692982, b1: 24784510, sigma1: 72213155, rho1_mag: 698633347, rho1_neg: false, m1_mag: 2408367, m1_neg: false, strike: 107470000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 155792, up0: 384159327, up1: 384159327 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 196559, b0: 18782526, sigma0: 119933968, rho0_mag: 945177297, rho0_neg: true, m0_mag: 2879451, m0_neg: true, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 196559, b1: 18782527, sigma1: 119933968, rho1_mag: 945177297, rho1_neg: true, m1_mag: 2879451, m1_neg: true, strike: 106860000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 157502, up0: 571763440, up1: 571763444 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 1321660, b0: 20043202, sigma0: 53468944, rho0_mag: 313520092, rho0_neg: true, m0_mag: 2927133, m0_neg: false, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 1321660, b1: 20043202, sigma1: 53468944, rho1_mag: 313520091, rho1_neg: true, m1_mag: 2927133, m1_neg: false, strike: 106743000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 150000, up0: 543065598, up1: 543065598 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 925326, b0: 31392101, sigma0: 58791810, rho0_mag: 494691427, rho0_neg: true, m0_mag: 191846, m0_neg: true, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 925326, b1: 31392101, sigma1: 58791810, rho1_mag: 494691426, rho1_neg: true, m1_mag: 191846, m1_neg: true, strike: 107449000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 150000, up0: 507899362, up1: 507899362 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 1380045, b0: 1430659, sigma0: 2638463, rho0_mag: 824832670, rho0_neg: false, m0_mag: 1001596, m0_neg: true, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 1380045, b1: 1430659, sigma1: 2638463, rho1_mag: 824832671, rho1_neg: false, m1_mag: 1001596, m1_neg: true, strike: 106486000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 150000, up0: 544146574, up1: 544146574 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 3996021, b0: 6730029, sigma0: 53809261, rho0_mag: 245217770, rho0_neg: true, m0_mag: 1343446, m0_neg: true, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 3996021, b1: 6730029, sigma1: 53809261, rho1_mag: 245217770, rho1_neg: true, m1_mag: 1343445, m1_neg: true, strike: 107160000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 150000, up0: 481726899, up1: 481726901 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 2010803, b0: 31075635, sigma0: 62433486, rho0_mag: 932788283, rho0_neg: false, m0_mag: 351416, m0_neg: false, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 2010803, b1: 31075635, sigma1: 62433486, rho1_mag: 932788283, rho1_neg: false, m1_mag: 351417, m1_neg: false, strike: 107214000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 150000, up0: 380971823, up1: 380971823 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 334555, b0: 13867931, sigma0: 20371496, rho0_mag: 810604133, rho0_neg: false, m0_mag: 2992423, m0_neg: false, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 334555, b1: 13867931, sigma1: 20371496, rho1_mag: 810604133, rho1_neg: false, m1_mag: 2992424, m1_neg: false, strike: 106657000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 150000, up0: 489041071, up1: 489041071 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 1441796, b0: 11865343, sigma0: 100480424, rho0_mag: 119475919, rho0_neg: false, m0_mag: 2967445, m0_neg: false, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 1441796, b1: 11865343, sigma1: 100480425, rho1_mag: 119475919, rho1_neg: false, m1_mag: 2967445, m1_neg: false, strike: 107064000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 150000, up0: 480699255, up1: 480699255 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 1175348, b0: 32638439, sigma0: 50139153, rho0_mag: 453979185, rho0_neg: false, m0_mag: 2876253, m0_neg: false, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 1175348, b1: 32638439, sigma1: 50139154, rho1_mag: 453979185, rho1_neg: false, m1_mag: 2876253, m1_neg: false, strike: 107160000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 150000, up0: 425727068, up1: 425727068 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 578000, b0: 26327939, sigma0: 74555942, rho0_mag: 549924499, rho0_neg: true, m0_mag: 2881559, m0_neg: false, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 578000, b1: 26327939, sigma1: 74555943, rho1_mag: 549924499, rho1_neg: true, m1_mag: 2881559, m1_neg: false, strike: 106486000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 150000, up0: 593181179, up1: 593181179 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 206963, b0: 33862461, sigma0: 75865281, rho0_mag: 731456866, rho0_neg: true, m0_mag: 2694344, m0_neg: false, spot1: 107000000000000, bs_forward1: 107000000000001, a1: 206963, b1: 33862461, sigma1: 75865281, rho1_mag: 731456866, rho1_neg: true, m1_mag: 2694344, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 0, up0: 586463895, up1: 586463895 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000001, a0: 206963, b0: 33862461, sigma0: 75865281, rho0_mag: 731456866, rho0_neg: true, m0_mag: 2694344, m0_neg: false, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 206963, b1: 33862461, sigma1: 75865281, rho1_mag: 731456866, rho1_neg: true, m1_mag: 2694344, m1_neg: false, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 0, up0: 586463895, up1: 586463895 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 931739, b0: 19719548, sigma0: 86579291, rho0_mag: 842701059, rho0_neg: false, m0_mag: 609387, m0_neg: true, spot1: 107053500000000, bs_forward1: 107053500000000, a1: 931444, b1: 19733724, sigma1: 86520951, rho1_mag: 842911295, rho1_neg: false, m1_mag: 609851, m1_neg: true, strike: 53500000000000, has_price: true, forward0: 107000000000000, forward1: 107053500000000, envelope: 125636791, up0: 1000000000, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 931739, b0: 19719548, sigma0: 86579291, rho0_mag: 842701059, rho0_neg: false, m0_mag: 609387, m0_neg: true, spot1: 107053500000000, bs_forward1: 107053500000000, a1: 931444, b1: 19733724, sigma1: 86520951, rho1_mag: 842911295, rho1_neg: false, m1_mag: 609851, m1_neg: true, strike: 101650000000000, has_price: true, forward0: 107000000000000, forward1: 107053500000000, envelope: 125636791, up0: 849624189, up1: 852589780 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 931739, b0: 19719548, sigma0: 86579291, rho0_mag: 842701059, rho0_neg: false, m0_mag: 609387, m0_neg: true, spot1: 107053500000000, bs_forward1: 107053500000000, a1: 931444, b1: 19733724, sigma1: 86520951, rho1_mag: 842911295, rho1_neg: false, m1_mag: 609851, m1_neg: true, strike: 106465000000000, has_price: true, forward0: 107000000000000, forward1: 107053500000000, envelope: 125636791, up0: 468023927, up1: 472370829 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 931739, b0: 19719548, sigma0: 86579291, rho0_mag: 842701059, rho0_neg: false, m0_mag: 609387, m0_neg: true, spot1: 107053500000000, bs_forward1: 107053500000000, a1: 931444, b1: 19733724, sigma1: 86520951, rho1_mag: 842911295, rho1_neg: false, m1_mag: 609851, m1_neg: true, strike: 106989000000000, has_price: true, forward0: 107000000000000, forward1: 107053500000000, envelope: 125636791, up0: 425683556, up1: 429844968 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 931739, b0: 19719548, sigma0: 86579291, rho0_mag: 842701059, rho0_neg: false, m0_mag: 609387, m0_neg: true, spot1: 107053500000000, bs_forward1: 107053500000000, a1: 931444, b1: 19733724, sigma1: 86520951, rho1_mag: 842911295, rho1_neg: false, m1_mag: 609851, m1_neg: true, strike: 107000000000000, has_price: true, forward0: 107000000000000, forward1: 107053500000000, envelope: 125636791, up0: 424816935, up1: 428973931 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 931739, b0: 19719548, sigma0: 86579291, rho0_mag: 842701059, rho0_neg: false, m0_mag: 609387, m0_neg: true, spot1: 107053500000000, bs_forward1: 107053500000000, a1: 931444, b1: 19733724, sigma1: 86520951, rho1_mag: 842911295, rho1_neg: false, m1_mag: 609851, m1_neg: true, strike: 107010000000000, has_price: true, forward0: 107000000000000, forward1: 107053500000000, envelope: 125636791, up0: 424029956, up1: 428182904 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 931739, b0: 19719548, sigma0: 86579291, rho0_mag: 842701059, rho0_neg: false, m0_mag: 609387, m0_neg: true, spot1: 107053500000000, bs_forward1: 107053500000000, a1: 931444, b1: 19733724, sigma1: 86520951, rho1_mag: 842911295, rho1_neg: false, m1_mag: 609851, m1_neg: true, strike: 107535000000000, has_price: true, forward0: 107000000000000, forward1: 107053500000000, envelope: 125636791, up0: 383918094, up1: 387840794 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 931739, b0: 19719548, sigma0: 86579291, rho0_mag: 842701059, rho0_neg: false, m0_mag: 609387, m0_neg: true, spot1: 107053500000000, bs_forward1: 107053500000000, a1: 931444, b1: 19733724, sigma1: 86520951, rho1_mag: 842911295, rho1_neg: false, m1_mag: 609851, m1_neg: true, strike: 112350000000000, has_price: true, forward0: 107000000000000, forward1: 107053500000000, envelope: 125636791, up0: 141953542, up1: 143575426 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 931739, b0: 19719548, sigma0: 86579291, rho0_mag: 842701059, rho0_neg: false, m0_mag: 609387, m0_neg: true, spot1: 107053500000000, bs_forward1: 107053500000000, a1: 931444, b1: 19733724, sigma1: 86520951, rho1_mag: 842911295, rho1_neg: false, m1_mag: 609851, m1_neg: true, strike: 160500000000000, has_price: true, forward0: 107000000000000, forward1: 107053500000000, envelope: 125636791, up0: 255055, up1: 258084 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 931739, b0: 19719548, sigma0: 86579291, rho0_mag: 842701059, rho0_neg: false, m0_mag: 609387, m0_neg: true, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 931444, b1: 19733724, sigma1: 86520951, rho1_mag: 842911295, rho1_neg: false, m1_mag: 609851, m1_neg: true, strike: 1000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 72792205, up0: 1000000000, up1: 1000000000 }, + DriftVector { spot0: 107000000000000, bs_forward0: 107000000000000, a0: 931739, b0: 19719548, sigma0: 86579291, rho0_mag: 842701059, rho0_neg: false, m0_mag: 609387, m0_neg: true, spot1: 107000000000000, bs_forward1: 107000000000000, a1: 931444, b1: 19733724, sigma1: 86520951, rho1_mag: 842911295, rho1_neg: false, m1_mag: 609851, m1_neg: true, strike: 9630000000000000, has_price: true, forward0: 107000000000000, forward1: 107000000000000, envelope: 72792205, up0: 0, up1: 0 }, + DriftVector { spot0: 63998382758170, bs_forward0: 63998382758170, a0: 413, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 87733, m0_neg: true, spot1: 63998399269771, bs_forward1: 63998399269771, a1: 413, b1: 100000, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 87733, m1_neg: true, strike: 64030000000000, has_price: true, forward0: 63998382758170, forward1: 63998399269771, envelope: 324526, up0: 246020872, up1: 246372415 }, + DriftVector { spot0: 60319118724041, bs_forward0: 60319118724041, a0: 292, b0: 30233, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 1826, m0_neg: true, spot1: 60319113207562, bs_forward1: 60319113207562, a1: 292, b1: 30233, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 1826, m1_neg: true, strike: 60374000000000, has_price: true, forward0: 60319118724041, forward1: 60319113207562, envelope: 218419, up0: 51086664, up1: 50789780 }, + DriftVector { spot0: 62799923415357, bs_forward0: 62799923415357, a0: 157, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 158464, m0_neg: true, spot1: 62799929426718, bs_forward1: 62799929426718, a1: 157, b1: 100000, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 158464, m1_neg: true, strike: 62773000000000, has_price: true, forward0: 62799923415357, forward1: 62799929426718, envelope: 252086, up0: 821510697, up1: 821133223 }, + DriftVector { spot0: 62771724282041, bs_forward0: 62771724282041, a0: 16, b0: 57140, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 95509, m0_neg: true, spot1: 62771769041949, bs_forward1: 62771769041949, a1: 16, b1: 57140, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 95509, m1_neg: true, strike: 62795000000000, has_price: true, forward0: 62771724282041, forward1: 62771769041949, envelope: 1996967, up0: 60651406, up1: 62794008 }, + DriftVector { spot0: 64116940097212, bs_forward0: 64116940097212, a0: 57, b0: 100000, sigma0: 1259903, rho0_mag: 931065023, rho0_neg: true, m0_mag: 1628538, m0_neg: false, spot1: 64116947004578, bs_forward1: 64116947004578, a1: 57, b1: 100000, sigma1: 1259903, rho1_mag: 931065023, rho1_neg: true, m1_mag: 1628538, m1_neg: false, strike: 64178000000000, has_price: true, forward0: 64116940097212, forward1: 64116947004578, envelope: 312852, up0: 41157630, up1: 41472897 }, + DriftVector { spot0: 60781923466668, bs_forward0: 60781923466668, a0: 375, b0: 66668, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 116554, m0_neg: true, spot1: 60781918195520, bs_forward1: 60781918195520, a1: 375, b1: 66668, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 116554, m1_neg: true, strike: 60802000000000, has_price: true, forward0: 60781923466668, forward1: 60781918195520, envelope: 209685, up0: 314642835, up1: 314389208 }, + DriftVector { spot0: 60777205891002, bs_forward0: 60777205891002, a0: 167, b0: 57140, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 129406, m0_neg: false, spot1: 60777240004559, bs_forward1: 60777240004559, a1: 167, b1: 57140, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 129406, m1_neg: false, strike: 60801000000000, has_price: true, forward0: 60777205891002, forward1: 60777240004559, envelope: 725480, up0: 208854521, up1: 209765959 }, + DriftVector { spot0: 59826336033859, bs_forward0: 59826336033859, a0: 189, b0: 33332, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 177988, m0_neg: false, spot1: 59826290229979, bs_forward1: 59826290229979, a1: 189, b1: 33332, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 177988, m1_neg: false, strike: 59856000000000, has_price: true, forward0: 59826336033859, forward1: 59826290229979, envelope: 875400, up0: 147059642, up1: 146109121 }, + DriftVector { spot0: 60734432572933, bs_forward0: 60734432572933, a0: 393, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 64518, m0_neg: false, spot1: 60734451625736, bs_forward1: 60734451625736, a1: 393, b1: 100000, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 64518, m1_neg: false, strike: 60771000000000, has_price: true, forward0: 60734432572933, forward1: 60734451625736, envelope: 368191, up0: 195281347, up1: 195675220 }, + DriftVector { spot0: 59968614807915, bs_forward0: 59968614807915, a0: 421, b0: 28572, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 1828683, m0_neg: true, spot1: 59968615145306, bs_forward1: 59968615145306, a1: 421, b1: 28572, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 1828683, m1_neg: true, strike: 59988000000000, has_price: true, forward0: 59968614807915, forward1: 59968615145306, envelope: 152503, up0: 311187727, up1: 311392987 }, + DriftVector { spot0: 62421793004534, bs_forward0: 62421793004534, a0: 126, b0: 39999, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 89719, m0_neg: true, spot1: 62421794246025, bs_forward1: 62421794246025, a1: 126, b1: 39999, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 89719, m1_neg: true, strike: 62389000000000, has_price: true, forward0: 62421793004534, forward1: 62421794246025, envelope: 170567, up0: 900837817, up1: 900237907 }, + DriftVector { spot0: 60342057303501, bs_forward0: 60342057303501, a0: 79, b0: 14286, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 227184, m0_neg: false, spot1: 60341924573180, bs_forward1: 60341924573180, a1: 79, b1: 14286, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 227184, m1_neg: false, strike: 60357000000000, has_price: true, forward0: 60342057303501, forward1: 60341924573180, envelope: 3315009, up0: 214555030, up1: 211188768 }, + DriftVector { spot0: 60638964859297, bs_forward0: 60638964859297, a0: 382, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 17545, m0_neg: true, spot1: 60638943333977, bs_forward1: 60638943333977, a1: 382, b1: 100000, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 17545, m1_neg: true, strike: 60676000000000, has_price: true, forward0: 60638964859297, forward1: 60638943333977, envelope: 401029, up0: 186882602, up1: 186460934 }, + DriftVector { spot0: 60239708983718, bs_forward0: 60239708983718, a0: 68, b0: 11764, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 218388, m0_neg: true, spot1: 60239835069067, bs_forward1: 60239835069067, a1: 68, b1: 11764, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 218388, m1_neg: true, strike: 60253000000000, has_price: true, forward0: 60239708983718, forward1: 60239835069067, envelope: 3369347, up0: 213541996, up1: 217306065 }, + DriftVector { spot0: 62547346956125, bs_forward0: 62547346956125, a0: 21, b0: 100000, sigma0: 1000000, rho0_mag: 940000000, rho0_neg: true, m0_mag: 57925, m0_neg: true, spot1: 62547335629767, bs_forward1: 62547335629767, a1: 21, b1: 100000, sigma1: 1000000, rho1_mag: 940000000, rho1_neg: true, m1_mag: 57925, m1_neg: true, strike: 62530000000000, has_price: true, forward0: 62547346956125, forward1: 62547335629767, envelope: 551724, up0: 813999031, up1: 814717627 }, + ] +} + +public fun spot0(v: &DriftVector): u64 { v.spot0 } + +public fun bs_forward0(v: &DriftVector): u64 { v.bs_forward0 } + +public fun a0(v: &DriftVector): u64 { v.a0 } + +public fun b0(v: &DriftVector): u64 { v.b0 } + +public fun sigma0(v: &DriftVector): u64 { v.sigma0 } + +public fun rho0_mag(v: &DriftVector): u64 { v.rho0_mag } + +public fun rho0_neg(v: &DriftVector): bool { v.rho0_neg } + +public fun m0_mag(v: &DriftVector): u64 { v.m0_mag } + +public fun m0_neg(v: &DriftVector): bool { v.m0_neg } + +public fun spot1(v: &DriftVector): u64 { v.spot1 } + +public fun bs_forward1(v: &DriftVector): u64 { v.bs_forward1 } + +public fun a1(v: &DriftVector): u64 { v.a1 } + +public fun b1(v: &DriftVector): u64 { v.b1 } + +public fun sigma1(v: &DriftVector): u64 { v.sigma1 } + +public fun rho1_mag(v: &DriftVector): u64 { v.rho1_mag } + +public fun rho1_neg(v: &DriftVector): bool { v.rho1_neg } + +public fun m1_mag(v: &DriftVector): u64 { v.m1_mag } + +public fun m1_neg(v: &DriftVector): bool { v.m1_neg } + +public fun strike(v: &DriftVector): u64 { v.strike } + +public fun has_price(v: &DriftVector): bool { v.has_price } + +public fun forward0(v: &DriftVector): u64 { v.forward0 } + +public fun forward1(v: &DriftVector): u64 { v.forward1 } + +public fun envelope(v: &DriftVector): u64 { v.envelope } + +public fun up0(v: &DriftVector): u64 { v.up0 } + +public fun up1(v: &DriftVector): u64 { v.up1 } diff --git a/packages/predict/tests/pricing/drift_replica_parity_tests.move b/packages/predict/tests/pricing/drift_replica_parity_tests.move new file mode 100644 index 000000000..e74370c1c --- /dev/null +++ b/packages/predict/tests/pricing/drift_replica_parity_tests.move @@ -0,0 +1,160 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// PARITY pin (not correctness coverage): bit-exact agreement between the +/// chain's `drift_envelope`/`up_price` and the DBU-557 measurement replica. +/// +/// The expected values here are the measurement replica's own outputs +/// (predict-research `scripts/drift_replica.py`, a deliberate fixed-point +/// parity model of pricing.move; generator `scripts/drift_fixture_gen.py`). +/// Per the unit-test rules that makes this a two-sided consistency contract, +/// NOT an independent correctness oracle: if pricing.move were wrong, both +/// sides would agree on the wrong number. Correctness is owned elsewhere — +/// `pricing_exact_tests` pins prices against an independent erf reference, +/// and the envelope's soundness claim is validated empirically by the +/// DBU-557 measurement (envelope >= realized sup over historical oracle +/// pairs). What THIS file guarantees: the off-chain measurement computes +/// exactly what the chain computes (the M0 fidelity gate — without it the +/// measurement's conclusions would be about the replica, not the contract), +/// and any future pricing.move change that shifts a covered value breaks +/// loudly, flagging that the measurement fixture must be regenerated. +/// +/// Each vector seeds its two oracle snapshots through the production ingest +/// (`prepare_real_oracle_bundle`, driving the fresh-Pyth forward round-trip), +/// loads real `Pricer`s, and asserts exact equality. Coverage: the zero +/// early-out, every reachable fail-closed reason (variance floor, forward +/// ratio guards, decay/band degeneracies incl. the round-down band collapse), +/// real captured SVI surfaces under perturbation ladders, one-ulp parameter +/// deltas, and wing/grid strikes. +#[test_only] +module deepbook_predict::drift_replica_parity_tests; + +use deepbook_predict::{drift_reference_data as ref_data, oracle_fixture, pricing, test_constants}; +use propbook::block_scholes_svi_feed::SVIParams; +use std::unit_test::assert_eq; + +/// Seed one snapshot's oracle values through the real ingest paths at an +/// explicit source timestamp (the oracle lanes keep the newest source +/// timestamp, so seeding two snapshots requires advancing it) and return the +/// loaded pricer's forward, SVI params, and the UP price at `strike` (0 when +/// the vector's surface cannot price). +fun snapshot( + fx: &mut oracle_fixture::OracleFixture, + oracle: &mut oracle_fixture::OracleBundle, + source_ts_ms: u64, + spot: u64, + bs_forward: u64, + a: u64, + b: u64, + sigma: u64, + rho_mag: u64, + rho_neg: bool, + m_mag: u64, + m_neg: bool, + strike: u64, + price: bool, +): (u64, SVIParams, u64) { + fx.set_pyth_bundle(oracle, spot, source_ts_ms); + fx.set_bs_spot_for_testing_bundle(oracle, source_ts_ms, spot); + fx.set_bs_forward_for_testing_bundle(oracle, source_ts_ms, bs_forward); + fx.set_bs_svi_for_testing_bundle( + oracle, + source_ts_ms, + a, + b, + sigma, + rho_mag, + rho_neg, + m_mag, + m_neg, + ); + let pricer = fx.load_pricer_bundle(oracle); + let up = if (price) pricer.up_price(strike) else 0; + (pricer.forward(), pricer.svi_params(), up) +} + +fun run_range(start: u64, end: u64) { + let mut fx = oracle_fixture::setup_oracle_default(); + let mut oracle = fx.take_oracle_bundle(); + let vectors = ref_data::vectors(); + let stop = end.min(vectors.length()); + let base_ts = test_constants::live_source_timestamp_ms(); + let mut i = start; + while (i < stop) { + let v = &vectors[i]; + let (forward0, svi0, up0) = snapshot( + &mut fx, + &mut oracle, + base_ts + 2 * (i - start), + v.spot0(), + v.bs_forward0(), + v.a0(), + v.b0(), + v.sigma0(), + v.rho0_mag(), + v.rho0_neg(), + v.m0_mag(), + v.m0_neg(), + v.strike(), + v.has_price(), + ); + let (forward1, svi1, up1) = snapshot( + &mut fx, + &mut oracle, + base_ts + 2 * (i - start) + 1, + v.spot1(), + v.bs_forward1(), + v.a1(), + v.b1(), + v.sigma1(), + v.rho1_mag(), + v.rho1_neg(), + v.m1_mag(), + v.m1_neg(), + v.strike(), + v.has_price(), + ); + assert_eq!(forward0, v.forward0()); + assert_eq!(forward1, v.forward1()); + let live = pricing::from_anchors( + sui::object::id_from_address(@0xD41F7), + forward1, + svi1, + ); + assert_eq!(live.drift_envelope(forward0, &svi0), v.envelope()); + if (v.has_price()) { + assert_eq!(up0, v.up0()); + assert_eq!(up1, v.up1()); + }; + i = i + 1; + }; + oracle_fixture::return_oracle_bundle(oracle); + fx.finish(); +} + +#[test] +fun drift_vectors_00_09() { run_range(0, 10) } + +#[test] +fun drift_vectors_10_19() { run_range(10, 20) } + +#[test] +fun drift_vectors_20_29() { run_range(20, 30) } + +#[test] +fun drift_vectors_30_39() { run_range(30, 40) } + +#[test] +fun drift_vectors_40_49() { run_range(40, 50) } + +#[test] +fun drift_vectors_50_59() { run_range(50, 60) } + +#[test] +fun drift_vectors_60_69() { run_range(60, 70) } + +#[test] +fun drift_vectors_70_79() { run_range(70, 80) } + +#[test] +fun drift_vectors_80_end() { run_range(80, 1000) }