diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d5921b29..fa6f2afdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: amm can use reference price offset from oracle price based on clamped inventory and persist market premiums ([#681](https://github.com/drift-labs/protocol-v2/pull/681)) + ### Fixes ### Breaking @@ -55,6 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - program: account for step size when canceling reduce only orders ### Breaking + - sdk: UserStatsMap use bulkAccountLoader (`UserStatsMap.subscribe` and `UserStatsMap.sync` now requires list of authorities) ([#716](https://github.com/drift-labs/protocol-v2/pull/716)) ## [2.47.0] - 2023-11-26 @@ -134,6 +137,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - program: add positive perp funding rate offset ([#576](https://github.com/drift-labs/protocol-v2/pull/576/files)) ### Fixes + - program: add validation check in update max imbalances ([#667](https://github.com/drift-labs/protocol-v2/pull/667)) ### Breaking diff --git a/Cargo.lock b/Cargo.lock index 7d355227e..4f7af2574 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -651,6 +651,7 @@ dependencies = [ "borsh", "bytemuck", "bytes", + "drift-macros", "enumflags2", "num-derive", "num-integer", @@ -661,10 +662,20 @@ dependencies = [ "serum_dex", "solana-program", "solana-security-txt", + "static_assertions", "thiserror", "uint", ] +[[package]] +name = "drift-macros" +version = "0.1.0" +source = "git+https://github.com/drift-labs/drift-macros.git?rev=c57d87#c57d87e073d13d43f4d1fb09fe6822915a4ccc11" +dependencies = [ + "quote", + "syn 1.0.92", +] + [[package]] name = "ed25519" version = "1.5.3" diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index b1799fd25..e6e61f499 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -33,6 +33,8 @@ serum_dex = { git = "https://github.com/project-serum/serum-dex", rev = "85b4f14 enumflags2 = "0.6.4" phoenix-v1 = { git = "https://github.com/drift-labs/phoenix-v1", rev = "4c65c9", version = "0.2.4", features = ["no-entrypoint"] } solana-security-txt = "1.1.0" +static_assertions = "1.1.0" +drift-macros = { git = "https://github.com/drift-labs/drift-macros.git", rev = "c57d87" } [dev-dependencies] bytes = "1.2.0" diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index ddae482a8..809e75e47 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -167,6 +167,31 @@ pub fn update_spread_reserves(amm: &mut AMM) -> DriftResult { } pub fn update_spreads(amm: &mut AMM, reserve_price: u64) -> DriftResult<(u32, u32)> { + let max_offset = amm.get_max_reference_price_offset()?; + + let reference_price_offset = if amm.curve_update_intensity > 0 { + let liquidity_ratio = amm_spread::calculate_inventory_liquidity_ratio( + amm.base_asset_amount_with_amm, + amm.base_asset_reserve, + amm.max_base_asset_reserve, + amm.min_base_asset_reserve, + )?; + + amm_spread::calculate_reference_price_offset( + reserve_price, + amm.last_24h_avg_funding_rate, + liquidity_ratio, + amm.min_order_size, + amm.historical_oracle_data.last_oracle_price_twap_5min, + amm.last_mark_price_twap_5min, + amm.historical_oracle_data.last_oracle_price_twap, + amm.last_mark_price_twap, + max_offset, + )? + } else { + 0 + }; + let (long_spread, short_spread) = if amm.curve_update_intensity > 0 { amm_spread::calculate_spread( amm.base_spread, @@ -196,6 +221,7 @@ pub fn update_spreads(amm: &mut AMM, reserve_price: u64) -> DriftResult<(u32, u3 amm.long_spread = long_spread; amm.short_spread = short_spread; + amm.reference_price_offset = reference_price_offset; update_spread_reserves(amm)?; diff --git a/programs/drift/src/controller/insurance/tests.rs b/programs/drift/src/controller/insurance/tests.rs index f50d575f7..32af4af4a 100644 --- a/programs/drift/src/controller/insurance/tests.rs +++ b/programs/drift/src/controller/insurance/tests.rs @@ -1534,7 +1534,7 @@ fn test_transfer_protocol_owned_stake() { .unwrap(); if_balance -= amount_returned; - assert_eq!(amount_returned, 99500000000); + assert_eq!(amount_returned, 99500000000_u64); assert_eq!(spot_market.insurance_fund.user_shares, 0); assert_eq!(spot_market.insurance_fund.total_shares, 21105203599); @@ -1597,7 +1597,7 @@ fn test_transfer_protocol_owned_stake() { let mut expected_if_stake_2 = InsuranceFundStake::new(Pubkey::default(), 0, 0); expected_if_stake_2 - .increase_if_shares(21105203599, &spot_market) + .increase_if_shares(21105203599_u128, &spot_market) .unwrap(); assert_eq!(user_stats_2.if_staked_quote_asset_amount, 99500000000); diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index 315edb36e..a70e4091e 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -727,7 +727,10 @@ pub fn handle_initialize_perp_market( padding1: 0, padding2: 0, total_fee_earned_per_lp: 0, - padding: [0; 32], + net_unsettled_funding_pnl: 0, + quote_asset_amount_with_unsettled_lp: 0, + reference_price_offset: 0, + padding: [0; 12], }, }; diff --git a/programs/drift/src/math/amm_spread.rs b/programs/drift/src/math/amm_spread.rs index 19f8dcffb..3334fd5f7 100644 --- a/programs/drift/src/math/amm_spread.rs +++ b/programs/drift/src/math/amm_spread.rs @@ -11,16 +11,15 @@ use crate::math::constants::{ AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO_I128, AMM_TO_QUOTE_PRECISION_RATIO_I128, BID_ASK_SPREAD_PRECISION, BID_ASK_SPREAD_PRECISION_I128, BID_ASK_SPREAD_PRECISION_U128, DEFAULT_LARGE_BID_ASK_FACTOR, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, - MAX_BID_ASK_INVENTORY_SKEW_FACTOR, PEG_PRECISION, PERCENTAGE_PRECISION, - PERCENTAGE_PRECISION_U64, PRICE_PRECISION, PRICE_PRECISION_I128, + FUNDING_RATE_BUFFER, MAX_BID_ASK_INVENTORY_SKEW_FACTOR, PEG_PRECISION, PERCENTAGE_PRECISION, + PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_U64, PRICE_PRECISION, PRICE_PRECISION_I128, + PRICE_PRECISION_I64, }; use crate::math::safe_math::SafeMath; use crate::state::perp_market::AMM; use crate::validate; -use super::constants::PERCENTAGE_PRECISION_I128; - #[cfg(test)] mod tests; @@ -328,8 +327,10 @@ pub fn calculate_spread( volume_24h, )?; - let mut long_spread = max((base_spread / 2) as u64, long_vol_spread); - let mut short_spread = max((base_spread / 2) as u64, short_vol_spread); + let half_base_spread_u64 = (base_spread / 2) as u64; + + let mut long_spread = max(half_base_spread_u64, long_vol_spread); + let mut short_spread = max(half_base_spread_u64, short_vol_spread); let max_target_spread = max_spread .cast::()? @@ -448,20 +449,23 @@ pub fn calculate_spread_reserves( PositionDirection::Short => amm.short_spread, }; + let spread_with_offset: i32 = spread.cast::()?.safe_add(amm.reference_price_offset)?; + let quote_asset_reserve_delta = if spread > 0 { amm.quote_asset_reserve - .safe_div(BID_ASK_SPREAD_PRECISION_U128 / (spread.cast::()? / 2))? + .safe_div(BID_ASK_SPREAD_PRECISION_U128 / (spread_with_offset.cast::()? / 2))? } else { 0 }; - let quote_asset_reserve = match direction { - PositionDirection::Long => amm - .quote_asset_reserve - .safe_add(quote_asset_reserve_delta)?, - PositionDirection::Short => amm - .quote_asset_reserve - .safe_sub(quote_asset_reserve_delta)?, + let quote_asset_reserve = if spread_with_offset >= 0 && direction == PositionDirection::Long + || spread_with_offset <= 0 && direction == PositionDirection::Short + { + amm.quote_asset_reserve + .safe_add(quote_asset_reserve_delta)? + } else { + amm.quote_asset_reserve + .safe_sub(quote_asset_reserve_delta)? }; let invariant_sqrt_u192 = U192::from(amm.sqrt_k); @@ -473,3 +477,75 @@ pub fn calculate_spread_reserves( Ok((base_asset_reserve, quote_asset_reserve)) } + +#[allow(clippy::comparison_chain)] +pub fn calculate_reference_price_offset( + reserve_price: u64, + last_24h_avg_funding_rate: i64, + liquidity_fraction: i128, + _min_order_size: u64, + oracle_twap_fast: i64, + mark_twap_fast: u64, + oracle_twap_slow: i64, + mark_twap_slow: u64, + max_offset_pct: i64, +) -> DriftResult { + if last_24h_avg_funding_rate == 0 { + return Ok(0); + } + + let max_offset_in_price = max_offset_pct + .safe_mul(reserve_price.cast()?)? + .safe_div(PERCENTAGE_PRECISION.cast()?)?; + + // calculate quote denominated market premium + let mark_premium_minute: i64 = mark_twap_fast + .cast::()? + .safe_sub(oracle_twap_fast)? + .clamp(-max_offset_in_price, max_offset_in_price); + let mark_premium_hour: i64 = mark_twap_slow + .cast::()? + .safe_sub(oracle_twap_slow)? + .clamp(-max_offset_in_price, max_offset_in_price); + // convert last_24h_avg_funding_rate to quote denominated premium + let mark_premium_day: i64 = last_24h_avg_funding_rate + .safe_div(FUNDING_RATE_BUFFER.cast()?)? + .safe_mul(24)? + .clamp(-max_offset_in_price, max_offset_in_price); // todo: look at how 24h funding is calc w.r.t. the funding_period + + // take average clamped premium as the price-based offset + let mark_premium_avg = mark_premium_minute + .safe_add(mark_premium_hour)? + .safe_add(mark_premium_day)? + .safe_div(3_i64)?; + + let mark_premium_avg_pct: i64 = mark_premium_avg + .safe_mul(PRICE_PRECISION_I64)? + .safe_div(reserve_price.cast()?)?; + + let inventory_pct = liquidity_fraction + .cast::()? + .safe_mul(max_offset_pct)? + .safe_div(PERCENTAGE_PRECISION.cast::()?)? + .clamp(-max_offset_pct, max_offset_pct); + + // only apply when inventory is consistent with recent and 24h market premium + let offset_pct = if (mark_premium_avg_pct >= 0 && inventory_pct >= 0) + || (mark_premium_avg_pct <= 0 && inventory_pct <= 0) + { + mark_premium_avg_pct.safe_add(inventory_pct)? + } else { + 0 + }; + + let clamped_offset_pct = offset_pct.clamp(-max_offset_pct, max_offset_pct); + + validate!( + clamped_offset_pct.abs() <= max_offset_pct, + ErrorCode::InvalidAmmDetected, + "clamp offset pct failed {}", + clamped_offset_pct + )?; + + clamped_offset_pct.cast() +} diff --git a/programs/drift/src/math/amm_spread/tests.rs b/programs/drift/src/math/amm_spread/tests.rs index b946b56c0..d01151607 100644 --- a/programs/drift/src/math/amm_spread/tests.rs +++ b/programs/drift/src/math/amm_spread/tests.rs @@ -37,6 +37,157 @@ mod test { assert_eq!(s, 104); } + #[test] + fn calculate_reference_price_offset_tests() { + let rev_price = 4216 * 10000; + let max_offset: i64 = 2500; // 25 bps + + let res = + calculate_reference_price_offset(rev_price, 0, 0, 0, 0, 0, 0, 0, max_offset).unwrap(); + assert_eq!(res, 0); + + let res = calculate_reference_price_offset( + rev_price, + 1, + 10, + 1, + 4216 * 10000, + 4217 * 10000, + 4216 * 10000, + 4217 * 10000, + max_offset, + ) + .unwrap(); + assert_eq!(res, 158); // 237*2/3); // 1 penny divergence + let res = calculate_reference_price_offset( + rev_price, + 1, + 10, + 1, + 4216 * 10000, + 4219 * 10000, + 4216 * 10000, + 4219 * 10000, + max_offset, + ) + .unwrap(); + assert_eq!(res, 237 * 2); // 3 penny divergence + + let res = calculate_reference_price_offset( + rev_price, + -43_000_000, + 10, + 1, + 4216 * 10000, + 4218 * 10000, + 4216 * 10000, + 4218 * 10000, + max_offset, + ) + .unwrap(); + assert_eq!(res, -517); // counter acting 24h_avg sign + + let res = calculate_reference_price_offset( + rev_price, + -43_000_000, + -10000, + 1, + 4216 * 10000, + 4218 * 10000, + 4216 * 10000, + 4218 * 10000, + max_offset, + ) + .unwrap(); + assert_eq!(res, -542); // counteracting 24h_avg / base inventory sign + + let res = calculate_reference_price_offset( + rev_price, + -43_000_000, + -10, + 1, + 4216 * 10000, + 4214 * 10000, + 4216 * 10000, + 4214 * 10000, + max_offset, + ) + .unwrap(); + assert_eq!(res, -1149); // flipped + + let res = calculate_reference_price_offset( + rev_price, + 1, + 10, + 1, + 4216 * 10000, + 4223 * 10000, + 4216 * 10000, + 4223 * 10000, + max_offset, + ) + .unwrap(); + assert_eq!(res, 1660 * 2 / 3); // 7 penny divergence + + let res = calculate_reference_price_offset( + rev_price, + 10_000_000, + 10, + 1, + 4216 * 10000, + 4233 * 10000, + 4216 * 10000, + 4233 * 10000, + max_offset, + ) + .unwrap(); + assert_eq!(res, 2500); // upper bound + + let res = calculate_reference_price_offset( + rev_price, + -10_000_000, + -10, + 1, + 4216 * 10000, + 4123 * 10000, + 4216 * 10000, + 4123 * 10000, + max_offset, + ) + .unwrap(); + assert_eq!(res, -2500); // lower bound + + // max offset = 0 + let res = calculate_reference_price_offset( + rev_price, + -10_000_000, + -10, + 1, + 4216 * 10000, + 4123 * 10000, + 6 * 10000, + 4123 * 10000, + 0, + ) + .unwrap(); + assert_eq!(res, 0); // zero bound + + // counteracting fast/slow twaps to 0 + let res = calculate_reference_price_offset( + rev_price, + -1, + 1, + 1, + 4216 * 10000, + 4123 * 10000, + 4123 * 10000, + 4216 * 10000, + max_offset, + ) + .unwrap(); + assert_eq!(res, 0); + } + #[test] fn calculate_spread_tests() { let base_spread = 1000; // .1% @@ -1178,6 +1329,60 @@ mod test { #[test] fn various_spread_tests() { + // should match typescript sdk tests in sdk/tests/amm/test.ts + + let (long_spread, short_spread) = calculate_spread( + 300, + 0, + 484, + 47500, + 923807816209694, + 925117623772584, + 13731157, + -1314027016625, + 13667686, + 115876379475, + 91316628, + 928097825691666, + 907979542352912, + 945977491145601, + 161188, + 1459632439, + 12358265776, + 72230366233, + 432067603632, + ) + .unwrap(); + assert_eq!(long_spread, 4262); + assert_eq!(short_spread, 43238); + + // terms 3 + let (long_spread, short_spread) = calculate_spread( + 300, + 0, + 484, + 47500, + 923807816209694, + 925117623772584, + 13731157, + -1314027016625, + 13667686, + 115876379475, + 91316628, + 928097825691666, + 907979542352912, + 945977491145601, + 161188, + 1459632439, + 12358265776, + 72230366233, + 432067603632, + ) + .unwrap(); + assert_eq!(long_spread, 4262); + assert_eq!(short_spread, 43238); + + // terms 4 let (long_spread, short_spread) = calculate_spread( 300, 0, @@ -1203,6 +1408,8 @@ mod test { assert_eq!(long_spread, 4262); assert_eq!(short_spread, 43238); + // extra one? + let (long_spread, short_spread) = calculate_spread( 300, 0, diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index 6d49bb7b8..67cc19b5d 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -12,7 +12,8 @@ use crate::math::constants::{ }; use crate::math::constants::{ AMM_RESERVE_PRECISION_I128, BID_ASK_SPREAD_PRECISION_U128, LP_FEE_SLICE_DENOMINATOR, - LP_FEE_SLICE_NUMERATOR, MARGIN_PRECISION_U128, SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR, + LP_FEE_SLICE_NUMERATOR, MARGIN_PRECISION_U128, PERCENTAGE_PRECISION, SPOT_WEIGHT_PRECISION, + TWENTY_FOUR_HOUR, }; use crate::math::helpers::get_proportion_i128; @@ -30,6 +31,9 @@ use crate::state::traits::{MarketIndexOffset, Size}; use crate::{AMM_TO_QUOTE_PRECISION_RATIO, PRICE_PRECISION}; use borsh::{BorshDeserialize, BorshSerialize}; +use drift_macros::assert_no_slop; +use static_assertions::const_assert_eq; + #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] pub enum MarketStatus { /// warm up period for initialization, fills are paused @@ -477,6 +481,7 @@ impl SpotBalance for PoolBalance { } } +#[assert_no_slop] #[zero_copy(unsafe)] #[derive(Debug, PartialEq, Eq)] #[repr(C)] @@ -654,7 +659,10 @@ pub struct AMM { pub padding1: u8, pub padding2: u16, pub total_fee_earned_per_lp: u64, - pub padding: [u8; 32], + pub net_unsettled_funding_pnl: i64, + pub quote_asset_amount_with_unsettled_lp: i64, + pub reference_price_offset: i32, + pub padding: [u8; 12], } impl Default for AMM { @@ -740,12 +748,23 @@ impl Default for AMM { padding1: 0, padding2: 0, total_fee_earned_per_lp: 0, - padding: [0; 32], + net_unsettled_funding_pnl: 0, + quote_asset_amount_with_unsettled_lp: 0, + reference_price_offset: 0, + padding: [0; 12], } } } impl AMM { + pub fn get_max_reference_price_offset(self) -> DriftResult { + // always allow 10 bps of price offset, up to a fifth of the market's max_spread + let ten_bps = PERCENTAGE_PRECISION.cast::()? / 1000; + let max_offset = (self.max_spread.cast::()? / 5).max(ten_bps); + + Ok(max_offset) + } + pub fn get_per_lp_base_unit(self) -> DriftResult { let scalar: i128 = 10_i128.pow(self.per_lp_base.abs().cast()?); diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index f7ccf30b2..a6957f2a4 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -1112,9 +1112,7 @@ export class DriftClient { ); } - public async getUserDeletionIx( - userAccountPublicKey: PublicKey - ) { + public async getUserDeletionIx(userAccountPublicKey: PublicKey) { const ix = await this.program.instruction.deleteUser({ accounts: { user: userAccountPublicKey, diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index b2dfddb2d..767709818 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -7343,12 +7343,24 @@ "name": "totalFeeEarnedPerLp", "type": "u64" }, + { + "name": "netUnsettledFundingPnl", + "type": "i64" + }, + { + "name": "quoteAssetAmountWithUnsettledLp", + "type": "i64" + }, + { + "name": "referencePriceOffset", + "type": "i32" + }, { "name": "padding", "type": { "array": [ "u8", - 32 + 12 ] } } diff --git a/sdk/src/math/amm.ts b/sdk/src/math/amm.ts index 6e25a7b9c..e0f622fa7 100644 --- a/sdk/src/math/amm.ts +++ b/sdk/src/math/amm.ts @@ -12,6 +12,7 @@ import { PRICE_DIV_PEG, PERCENTAGE_PRECISION, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, + FUNDING_RATE_BUFFER_PRECISION, TWO, } from '../constants/numericConstants'; import { @@ -22,7 +23,7 @@ import { isVariant, } from '../types'; import { assert } from '../assert/assert'; -import { squareRootBN, clampBN, standardizeBaseAssetAmount } from '..'; +import { squareRootBN, sigNum, clampBN, standardizeBaseAssetAmount } from '..'; import { OraclePriceData } from '../oracles/types'; import { @@ -352,21 +353,12 @@ export function calculateMarketOpenBidAsk( return [openBids, openAsks]; } -export function calculateInventoryScale( +export function calculateInventoryLiquidityRatio( baseAssetAmountWithAmm: BN, baseAssetReserve: BN, minBaseAssetReserve: BN, - maxBaseAssetReserve: BN, - directionalSpread: number, - maxSpread: number -): number { - if (baseAssetAmountWithAmm.eq(ZERO)) { - return 1; - } - - const MAX_BID_ASK_INVENTORY_SKEW_FACTOR = BID_ASK_SPREAD_PRECISION.mul( - new BN(10) - ); + maxBaseAssetReserve: BN +): BN { // inventory skew const [openBids, openAsks] = calculateMarketOpenBidAsk( baseAssetReserve, @@ -383,6 +375,31 @@ export function calculateInventoryScale( .abs(), PERCENTAGE_PRECISION ); + return inventoryScaleBN; +} + +export function calculateInventoryScale( + baseAssetAmountWithAmm: BN, + baseAssetReserve: BN, + minBaseAssetReserve: BN, + maxBaseAssetReserve: BN, + directionalSpread: number, + maxSpread: number +): number { + if (baseAssetAmountWithAmm.eq(ZERO)) { + return 1; + } + + const MAX_BID_ASK_INVENTORY_SKEW_FACTOR = BID_ASK_SPREAD_PRECISION.mul( + new BN(10) + ); + + const inventoryScaleBN = calculateInventoryLiquidityRatio( + baseAssetAmountWithAmm, + baseAssetReserve, + minBaseAssetReserve, + maxBaseAssetReserve + ); const inventoryScaleMaxBN = BN.max( MAX_BID_ASK_INVENTORY_SKEW_FACTOR, @@ -402,6 +419,76 @@ export function calculateInventoryScale( return inventoryScaleCapped; } +export function calculateReferencePriceOffset( + reservePrice: BN, + last24hAvgFundingRate: BN, + liquidityFraction: BN, + oracleTwapFast: BN, + markTwapFast: BN, + oracleTwapSlow: BN, + markTwapSlow: BN, + maxOffsetPct: number +): BN { + if (last24hAvgFundingRate.eq(ZERO)) { + return ZERO; + } + + const maxOffsetInPrice = new BN(maxOffsetPct) + .mul(reservePrice) + .div(PERCENTAGE_PRECISION); + + // Calculate quote denominated market premium + const markPremiumMinute = clampBN( + markTwapFast.sub(oracleTwapFast), + maxOffsetInPrice.mul(new BN(-1)), + maxOffsetInPrice + ); + + const markPremiumHour = clampBN( + markTwapSlow.sub(oracleTwapSlow), + maxOffsetInPrice.mul(new BN(-1)), + maxOffsetInPrice + ); + + // Convert last24hAvgFundingRate to quote denominated premium + const markPremiumDay = clampBN( + last24hAvgFundingRate.div(FUNDING_RATE_BUFFER_PRECISION).mul(new BN(24)), + maxOffsetInPrice.mul(new BN(-1)), + maxOffsetInPrice + ); + + // Take average clamped premium as the price-based offset + const markPremiumAvg = markPremiumMinute + .add(markPremiumHour) + .add(markPremiumDay) + .div(new BN(3)); + + const markPremiumAvgPct = markPremiumAvg + .mul(PRICE_PRECISION) + .div(reservePrice); + + const inventoryPct = clampBN( + liquidityFraction.mul(new BN(maxOffsetPct)).div(PERCENTAGE_PRECISION), + maxOffsetInPrice.mul(new BN(-1)), + maxOffsetInPrice + ); + + // Only apply when inventory is consistent with recent and 24h market premium + let offsetPct = markPremiumAvgPct.add(inventoryPct); + + if (!sigNum(inventoryPct).eq(sigNum(markPremiumAvgPct))) { + offsetPct = ZERO; + } + + const clampedOffsetPct = clampBN( + offsetPct, + new BN(-maxOffsetPct), + new BN(maxOffsetPct) + ); + + return clampedOffsetPct; +} + export function calculateEffectiveLeverage( baseSpread: number, quoteAssetReserve: BN, @@ -514,6 +601,11 @@ export function calculateSpreadBN( ) { assert(Number.isInteger(baseSpread)); assert(Number.isInteger(maxSpread)); + console.log('max spread;', maxSpread); + console.log( + 'lastOracleReservePriceSpreadPct:', + lastOracleReservePriceSpreadPct.toNumber() + ); const spreadTerms = { longVolSpread: 0, @@ -532,6 +624,8 @@ export function calculateSpreadBN( halfRevenueRetreatAmount: 0, longSpreadwRevRetreat: 0, shortSpreadwRevRetreat: 0, + longSpreadwOffsetShrink: 0, + shortSpreadwOffsetShrink: 0, totalSpread: 0, longSpread: 0, shortSpread: 0, @@ -665,6 +759,7 @@ export function calculateSpreadBN( spreadTerms.shortSpreadwRevRetreat = shortSpread; const totalSpread = longSpread + shortSpread; + console.log(totalSpread, maxTargetSpread); if (totalSpread > maxTargetSpread) { if (longSpread > shortSpread) { longSpread = Math.ceil((longSpread * maxTargetSpread) / totalSpread); @@ -675,10 +770,12 @@ export function calculateSpreadBN( } } + console.log(maxTargetSpread, totalSpread); + spreadTerms.totalSpread = totalSpread; spreadTerms.longSpread = longSpread; spreadTerms.shortSpread = shortSpread; - + console.log(spreadTerms); if (returnTerms) { return spreadTerms; } @@ -688,17 +785,20 @@ export function calculateSpreadBN( export function calculateSpread( amm: AMM, oraclePriceData: OraclePriceData, - now?: BN + now?: BN, + reservePrice?: BN ): [number, number] { if (amm.baseSpread == 0 || amm.curveUpdateIntensity == 0) { return [amm.baseSpread / 2, amm.baseSpread / 2]; } - const reservePrice = calculatePrice( - amm.baseAssetReserve, - amm.quoteAssetReserve, - amm.pegMultiplier - ); + if (!reservePrice) { + reservePrice = calculatePrice( + amm.baseAssetReserve, + amm.quoteAssetReserve, + amm.pegMultiplier + ); + } const targetPrice = oraclePriceData?.price || reservePrice; const confInterval = oraclePriceData.confidence || ZERO; @@ -735,6 +835,7 @@ export function calculateSpread( amm.shortIntensityVolume, amm.volume24H ); + console.log('amm.maxSpread:', amm.maxSpread.toFixed(2)); const longSpread = spreads[0]; const shortSpread = spreads[1]; @@ -760,18 +861,36 @@ export function calculateSpreadReserves( quoteAssetReserve: amm.quoteAssetReserve, }; } - const spreadFraction = BN.max(new BN(spread / 2), ONE); + let spreadFraction = new BN(spread / 2); + + // make non-zero + if (spreadFraction.eq(ZERO)) { + spreadFraction = spread >= 0 ? new BN(1) : new BN(-1); + console.log('spreadFractioN:', spreadFraction); + } + if (spreadFraction.gt(BID_ASK_SPREAD_PRECISION)) { + console.log('spreadFractioN ERRR:', spreadFraction.toNumber()); + } + const quoteAssetReserveDelta = amm.quoteAssetReserve.div( BID_ASK_SPREAD_PRECISION.div(spreadFraction) ); let quoteAssetReserve; - if (isVariant(direction, 'long')) { + if ( + (spread >= 0 && isVariant(direction, 'long')) || + (spread <= 0 && isVariant(direction, 'short')) + ) { quoteAssetReserve = amm.quoteAssetReserve.add(quoteAssetReserveDelta); } else { quoteAssetReserve = amm.quoteAssetReserve.sub(quoteAssetReserveDelta); } - + console.log( + 'amm.sqrtK:', + amm.sqrtK.toString(), + amm.sqrtK.mul(amm.sqrtK).toString(), + quoteAssetReserve.toString() + ); const baseAssetReserve = amm.sqrtK.mul(amm.sqrtK).div(quoteAssetReserve); return { baseAssetReserve, @@ -779,14 +898,52 @@ export function calculateSpreadReserves( }; } - const [longSpread, shortSpread] = calculateSpread(amm, oraclePriceData, now); + const reservePrice = calculatePrice( + amm.baseAssetReserve, + amm.quoteAssetReserve, + amm.pegMultiplier + ); + + // always allow 10 bps of price offset, up to a fifth of the market's max_spread + const maxOffset = Math.max( + amm.maxSpread / 5, + PERCENTAGE_PRECISION.toNumber() / 1000 + ); + const liquidityFraction = calculateInventoryLiquidityRatio( + amm.baseAssetAmountWithAmm, + amm.baseAssetReserve, + amm.minBaseAssetReserve, + amm.maxBaseAssetReserve + ); + const referencePriceOffset = calculateReferencePriceOffset( + reservePrice, + amm.last24HAvgFundingRate, + liquidityFraction, + amm.historicalOracleData.lastOraclePriceTwap5Min, + amm.lastMarkPriceTwap5Min, + amm.historicalOracleData.lastOraclePriceTwap, + amm.lastMarkPriceTwap, + maxOffset + ); + + console.log('referencePriceOffset:', referencePriceOffset.toNumber()); + + const [longSpread, shortSpread] = calculateSpread( + amm, + oraclePriceData, + now, + reservePrice + ); + console.log('longSpread:', longSpread); + console.log('shortSpread:', shortSpread); + const askReserves = calculateSpreadReserve( - longSpread, + longSpread + referencePriceOffset.toNumber(), PositionDirection.LONG, amm ); const bidReserves = calculateSpreadReserve( - shortSpread, + shortSpread + referencePriceOffset.toNumber(), PositionDirection.SHORT, amm ); diff --git a/sdk/src/types.ts b/sdk/src/types.ts index 183a9168a..379d4be0e 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -710,7 +710,7 @@ export type AMM = { pegMultiplier: BN; cumulativeFundingRateLong: BN; cumulativeFundingRateShort: BN; - last24hAvgFundingRate: BN; + last24HAvgFundingRate: BN; lastFundingRateShort: BN; lastFundingRateLong: BN; diff --git a/sdk/tests/amm/test.ts b/sdk/tests/amm/test.ts index dae72d874..9fb081cd6 100644 --- a/sdk/tests/amm/test.ts +++ b/sdk/tests/amm/test.ts @@ -4,6 +4,8 @@ import { PRICE_PRECISION, AMM_RESERVE_PRECISION, QUOTE_PRECISION, + PERCENTAGE_PRECISION, + calculateSpread, calculateSpreadBN, ZERO, ONE, @@ -19,6 +21,12 @@ import { L2Level, calculateUpdatedAMM, calculateMarketOpenBidAsk, + calculateSpreadReserves, + calculatePrice, + BID_ASK_SPREAD_PRECISION, + squareRootBN, + calculateReferencePriceOffset, + calculateInventoryLiquidityRatio, } from '../../src'; import { mockPerpMarkets } from '../dlob/helpers'; @@ -293,7 +301,7 @@ describe('AMM Tests', () => { volume24H, true ); - console.log(terms1); + // console.log(terms1); console.log('long/short spread:', l1, s1); assert(l1 == 14864); @@ -326,11 +334,77 @@ describe('AMM Tests', () => { true ); - console.log(terms2); + // console.log(terms2); assert(terms2.effectiveLeverageCapped >= 1.0002); assert(terms2.inventorySpreadScale == 1.73492); assert(terms2.longSpread == 4262); assert(terms2.shortSpread == 43238); + + // add spread offset + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const terms3: AMMSpreadTerms = calculateSpreadBN( + 300, + new BN(0), + new BN(484), + 47500, + new BN(923807816209694), + new BN(925117623772584), + new BN(13731157), + new BN(-1314027016625), + new BN(13667686), + new BN(115876379475), + new BN(91316628), + new BN(928097825691666), + new BN(907979542352912), + new BN(945977491145601), + new BN(161188), + new BN(1459632439), + new BN(12358265776), + new BN(72230366233), + new BN(432067603632), + true + ); + + console.log(terms3); + assert(terms3.effectiveLeverageCapped >= 1.0002); + assert(terms3.inventorySpreadScale == 1.73492); + assert(terms3.longSpread == 4262); + assert(terms3.shortSpread == 43238); + assert(terms3.longSpread + terms3.shortSpread == 47500); + + // add spread offset + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const terms4: AMMSpreadTerms = calculateSpreadBN( + 300, + new BN(0), + new BN(484), + 47500, + new BN(923807816209694), + new BN(925117623772584), + new BN(13731157), + new BN(-1314027016625), + new BN(13667686), + new BN(115876379475), + new BN(91316628), + new BN(928097825691666), + new BN(907979542352912), + new BN(945977491145601), + new BN(161188), + new BN(1459632439), + new BN(12358265776), + new BN(72230366233), + new BN(432067603632), + true + ); + + console.log(terms4); + assert(terms4.effectiveLeverageCapped >= 1.0002); + assert(terms4.inventorySpreadScale == 1.73492); + assert(terms4.longSpread == 4262); + assert(terms4.shortSpread == 43238); + assert(terms4.longSpread + terms4.shortSpread == 47500); }); it('Corner Case Spreads', () => { @@ -361,16 +435,150 @@ describe('AMM Tests', () => { console.log(terms2); assert(terms2.effectiveLeverageCapped <= 1.000001); - assert( - terms2.inventorySpreadScale == 1.013527, - `got: ${terms2.inventorySpreadScale}` + assert(terms2.inventorySpreadScale == 1.0306); + assert(terms2.longSpread == 515); + assert(terms2.shortSpread == 5668); + }); + + it('Spread Reserves (with offset)', () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const mockMarket1 = myMockPerpMarkets[0]; + let mockAmm = mockMarket1.amm; + const now = new BN(new Date().getTime() / 1000); //todo + + const oraclePriceData = { + price: new BN(13.553 * PRICE_PRECISION.toNumber()), + slot: new BN(68 + 1), + confidence: new BN(1), + hasSufficientNumberOfDataPoints: true, + }; + + const reserves = calculateSpreadReserves(mockAmm, oraclePriceData, now); + assert(reserves[0].baseAssetReserve.eq(new BN('1000000000'))); + assert(reserves[0].quoteAssetReserve.eq(new BN('12000000000'))); + assert(reserves[1].baseAssetReserve.eq(new BN('1000000000'))); + assert(reserves[1].quoteAssetReserve.eq(new BN('12000000000'))); + + mockAmm.baseAssetReserve = new BN(1000000000); + mockAmm.quoteAssetReserve = new BN(1000000000); + mockAmm.sqrtK = new BN(1000000000); + + mockAmm.baseAssetAmountWithAmm = new BN(0); + mockAmm.pegMultiplier = new BN(13.553 * PEG_PRECISION.toNumber()); + mockAmm.ammJitIntensity = 100; + mockAmm.curveUpdateIntensity = 200; + mockAmm.baseSpread = 2500; + mockAmm.maxSpread = 25000; + + mockAmm.last24HAvgFundingRate = new BN(7590328523); + + mockAmm.lastMarkPriceTwap = new BN( + (oraclePriceData.price.toNumber() / 1e6 - 0.01) * 1e6 + ); + mockAmm.historicalOracleData.lastOraclePriceTwap = new BN( + (oraclePriceData.price.toNumber() / 1e6 + 0.015) * 1e6 ); - assert(terms2.longSpread == 1146); - assert(terms2.shortSpread == 6686); + + mockAmm.historicalOracleData.lastOraclePriceTwap5Min = new BN( + (oraclePriceData.price.toNumber() / 1e6 + 0.005) * 1e6 + ); + mockAmm.lastMarkPriceTwap5Min = new BN( + (oraclePriceData.price.toNumber() / 1e6 - 0.005) * 1e6 + ); + + console.log('starting rr:'); + let reservePrice = undefined; + if (!reservePrice) { + reservePrice = calculatePrice( + mockAmm.baseAssetReserve, + mockAmm.quoteAssetReserve, + mockAmm.pegMultiplier + ); + } + + const targetPrice = oraclePriceData?.price || reservePrice; + const confInterval = oraclePriceData.confidence || ZERO; + const targetMarkSpreadPct = reservePrice + .sub(targetPrice) + .mul(BID_ASK_SPREAD_PRECISION) + .div(reservePrice); + + const confIntervalPct = confInterval + .mul(BID_ASK_SPREAD_PRECISION) + .div(reservePrice); + + // now = now || new BN(new Date().getTime() / 1000); //todo + const liveOracleStd = calculateLiveOracleStd(mockAmm, oraclePriceData, now); + console.log('reservePrice:', reservePrice.toString()); + console.log('targetMarkSpreadPct:', targetMarkSpreadPct.toString()); + console.log('confIntervalPct:', confIntervalPct.toString()); + + console.log('liveOracleStd:', liveOracleStd.toString()); + + const tt = calculateSpread(mockAmm, oraclePriceData, now); + console.log(tt); + + console.log('amm.baseAssetReserve:', mockAmm.baseAssetReserve.toString()); + assert(mockAmm.baseAssetReserve.eq(new BN('1000000000'))); + const reserves2 = calculateSpreadReserves(mockAmm, oraclePriceData, now); + console.log(reserves2[1].baseAssetReserve.toString()); + console.log(reserves2[1].quoteAssetReserve.toString()); + + assert(reserves2[0].baseAssetReserve.eq(new BN('1006711408'))); + assert(reserves2[0].quoteAssetReserve.eq(new BN('993333334'))); + assert(reserves2[1].baseAssetReserve.eq(new BN('993377484'))); + assert(reserves2[1].quoteAssetReserve.eq(new BN('1006666666'))); + + // create imbalance for reference price offset + mockAmm.baseAssetReserve = new BN(1000000000 * 1.1); + mockAmm.quoteAssetReserve = new BN(1000000000 / 1.1); + mockAmm.sqrtK = squareRootBN( + mockAmm.baseAssetReserve.mul(mockAmm.quoteAssetReserve) + ); + + mockAmm.baseAssetAmountWithAmm = new BN(-1000000000 * 0.1); + + const maxOffset = Math.max( + mockAmm.maxSpread / 5, + PERCENTAGE_PRECISION.toNumber() / 1000 + ); + const liquidityFraction = calculateInventoryLiquidityRatio( + mockAmm.baseAssetAmountWithAmm, + mockAmm.baseAssetReserve, + mockAmm.minBaseAssetReserve, + mockAmm.maxBaseAssetReserve + ); + console.log('liquidityFraction:', liquidityFraction.toString()); + assert(liquidityFraction.eq(new BN(1000000))); // full + + const referencePriceOffset = calculateReferencePriceOffset( + reservePrice, + mockAmm.last24HAvgFundingRate, + liquidityFraction, + mockAmm.historicalOracleData.lastOraclePriceTwap5Min, + mockAmm.lastMarkPriceTwap5Min, + mockAmm.historicalOracleData.lastOraclePriceTwap, + mockAmm.lastMarkPriceTwap, + maxOffset + ); + console.log('referencePriceOffset:', referencePriceOffset.toString()); + assert(referencePriceOffset.eq(new BN(5000))); + assert(referencePriceOffset.eq(new BN(maxOffset))); + + const reserves3 = calculateSpreadReserves(mockAmm, oraclePriceData, now); + console.log(reserves3[1].baseAssetReserve.toString()); + console.log(reserves3[1].quoteAssetReserve.toString()); + + assert(reserves3[0].baseAssetReserve.eq(new BN('1164705879'))); + assert(reserves3[0].quoteAssetReserve.eq(new BN('858585859'))); + assert(reserves3[1].baseAssetReserve.eq(new BN('1042105261'))); + assert(reserves3[1].quoteAssetReserve.eq(new BN('959595959'))); }); it('live update functions', () => { - const mockAmm = mockPerpMarkets[0].amm; + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const mockMarket1 = myMockPerpMarkets[0]; + const mockAmm = mockMarket1.amm; const now = new BN(new Date().getTime() / 1000); //todo const oraclePriceData = { @@ -469,8 +677,8 @@ describe('AMM Tests', () => { assert(markTwapLive.eq(new BN('1949826'))); assert(oracleTwapLive.eq(new BN('1942510'))); - assert(est1.eq(new BN('15692')), `got: ${est1}`); - assert(est2.eq(new BN('15692'))); + assert(est1.eq(new BN('16525'))); + assert(est2.eq(new BN('16525'))); }); it('predicted funding rate mock2', async () => { @@ -559,7 +767,7 @@ describe('AMM Tests', () => { assert(markTwapLive.eq(new BN('1222131'))); assert(oracleTwapLive.eq(new BN('1222586'))); assert(est1.eq(est2)); - assert(est2.eq(new BN('-1550')), `got: ${est2}`); + assert(est2.eq(new BN('-719'))); }); it('orderbook L2 gen (no topOfBookQuoteAmounts, 10 numOrders, low liquidity)', async () => { diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index 3c180d78c..ee98900d3 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -84,7 +84,7 @@ export const mockAMM: AMM = { baseAssetAmountWithUnsettledLp: new BN(0), orderStepSize: new BN(0), orderTickSize: new BN(1), - last24hAvgFundingRate: new BN(0), + last24HAvgFundingRate: new BN(0), lastFundingRateShort: new BN(0), lastFundingRateLong: new BN(0), concentrationCoef: new BN(0), diff --git a/sdk/tests/dlob/test.ts b/sdk/tests/dlob/test.ts index 516182d5d..fd2268f1f 100644 --- a/sdk/tests/dlob/test.ts +++ b/sdk/tests/dlob/test.ts @@ -2740,8 +2740,13 @@ describe('DLOB Perp Tests', () => { ); expect(takingBids.length).to.equal(1); const triggerLimitBid = takingBids[0]; - expect(isAuctionComplete(triggerLimitBid.order!, slot)).to.equal(true); - expect(isRestingLimitOrder(triggerLimitBid.order!, slot)).to.equal(false); + expect(triggerLimitBid !== undefined); + expect(isAuctionComplete(triggerLimitBid.order as Order, slot)).to.equal( + true + ); + expect(isRestingLimitOrder(triggerLimitBid.order as Order, slot)).to.equal( + false + ); }); it('Test will return expired market orders to fill', () => { diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index ac5711ff4..9a9bcef68 100644 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -4,7 +4,7 @@ if [ "$1" != "--skip-build" ] cp target/idl/drift.json sdk/src/idl/ fi -test_files=(liquidityProvider.ts) +test_files=(perpLpJit.ts) for test_file in ${test_files[@]}; do ANCHOR_TEST_FILE=${test_file} anchor test --skip-build || exit 1; diff --git a/tests/perpLpJit.ts b/tests/perpLpJit.ts index 5bc191de5..312495eee 100644 --- a/tests/perpLpJit.ts +++ b/tests/perpLpJit.ts @@ -780,9 +780,9 @@ describe('lp jit', () => { }); await traderDriftClient.placePerpOrder(takerOrderParams); await traderDriftClient.fetchAccounts(); - console.log(takerOrderParams); + // console.log(takerOrderParams); const order = traderDriftClient.getUser().getOrderByUserOrderId(1); - console.log(order); + // console.log(order); assert(!order.postOnly); @@ -795,7 +795,7 @@ describe('lp jit', () => { postOnly: PostOnlyParams.MUST_POST_ONLY, immediateOrCancel: true, }); - console.log('maker:', makerOrderParams); + // console.log('maker:', makerOrderParams); const txSig = await poorDriftClient.placeAndMakePerpOrder( makerOrderParams,