From b1ff36fdbf0d3f332155b0f1e3a2b92512599696 Mon Sep 17 00:00:00 2001 From: lil perp Date: Mon, 6 Feb 2023 15:11:43 -0500 Subject: [PATCH] program: flag to set max leverage for spot orders (#346) * program: flag to set max leverage for spot orders * allow max leverage flag for perps * add ts test * fix lint issue * fix tick/step size mix up --- programs/drift/src/controller/orders.rs | 54 +- programs/drift/src/math/margin.rs | 23 +- programs/drift/src/math/orders.rs | 266 ++++++- programs/drift/src/math/orders/tests.rs | 878 ++++++++++++++++++++++++ programs/drift/src/state/spot_market.rs | 2 +- programs/drift/src/state/user.rs | 20 +- programs/drift/src/state/user/tests.rs | 16 +- sdk/src/constants/numericConstants.ts | 1 + test-scripts/run-anchor-tests.sh | 1 + tests/maxLeverageOrderParams.ts | 214 ++++++ 10 files changed, 1434 insertions(+), 41 deletions(-) create mode 100644 tests/maxLeverageOrderParams.ts diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 38c3c1bc5..9da15fb5a 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -175,9 +175,6 @@ pub fn place_perp_order( // Increment open orders for existing position let (existing_position_direction, order_base_asset_amount) = { - let market_position = &mut user.perp_positions[position_index]; - market_position.open_orders += 1; - validate!( params.base_asset_amount >= market.amm.order_step_size, ErrorCode::OrderAmountTooSmall, @@ -186,8 +183,22 @@ pub fn place_perp_order( market.amm.order_step_size )?; - let base_asset_amount = - standardize_base_asset_amount(params.base_asset_amount, market.amm.order_step_size)?; + let base_asset_amount = if params.base_asset_amount == u64::MAX { + calculate_max_perp_order_size( + user, + position_index, + params.market_index, + params.direction, + perp_market_map, + spot_market_map, + oracle_map, + )? + } else { + standardize_base_asset_amount(params.base_asset_amount, market.amm.order_step_size)? + }; + + let market_position = &mut user.perp_positions[position_index]; + market_position.open_orders += 1; if !matches!( ¶ms.order_type, @@ -2626,6 +2637,7 @@ pub fn place_spot_order( let market_index = params.market_index; let spot_market = &spot_market_map.get_ref(&market_index)?; let force_reduce_only = spot_market.is_reduce_only()?; + let step_size = spot_market.order_step_size; validate!( !matches!(spot_market.status, MarketStatus::Initialized), @@ -2639,7 +2651,7 @@ pub fn place_spot_order( let oracle_price_data = *oracle_map.get_price_data(&spot_market.oracle)?; let (worst_case_token_amount_before, _) = user.spot_positions[spot_position_index] - .get_worst_case_token_amounts(spot_market, &oracle_price_data, None)?; + .get_worst_case_token_amounts(spot_market, &oracle_price_data, None, None)?; let balance_type = user.spot_positions[spot_position_index].balance_type; let token_amount = user.spot_positions[spot_position_index].get_token_amount(spot_market)?; @@ -2647,28 +2659,38 @@ pub fn place_spot_order( // Increment open orders for existing position let (existing_position_direction, order_base_asset_amount) = { - let spot_position = &mut user.spot_positions[spot_position_index]; - spot_position.open_orders += 1; - validate!( - params.base_asset_amount >= spot_market.order_step_size, + params.base_asset_amount >= step_size, ErrorCode::InvalidOrderSizeTooSmall, "params.base_asset_amount={} cannot be below spot_market.order_step_size={}", params.base_asset_amount, - spot_market.order_step_size + step_size )?; - let base_asset_amount = - standardize_base_asset_amount(params.base_asset_amount, spot_market.order_step_size)?; + let base_asset_amount = if params.base_asset_amount == u64::MAX { + calculate_max_spot_order_size( + user, + params.market_index, + params.direction, + perp_market_map, + spot_market_map, + oracle_map, + )? + } else { + standardize_base_asset_amount(params.base_asset_amount, step_size)? + }; validate!( - is_multiple_of_step_size(base_asset_amount, spot_market.order_step_size)?, + is_multiple_of_step_size(base_asset_amount, step_size)?, ErrorCode::InvalidOrderNotStepSizeMultiple, "Order base asset amount ({}), is not a multiple of step size ({})", base_asset_amount, - spot_market.order_step_size + step_size )?; + let spot_position = &mut user.spot_positions[spot_position_index]; + spot_position.open_orders += 1; + if !matches!( ¶ms.order_type, OrderType::TriggerMarket | OrderType::TriggerLimit @@ -2754,7 +2776,7 @@ pub fn place_spot_order( user.orders[new_order_index] = new_order; let (worst_case_token_amount_after, _) = user.spot_positions[spot_position_index] - .get_worst_case_token_amounts(spot_market, &oracle_price_data, None)?; + .get_worst_case_token_amounts(spot_market, &oracle_price_data, None, None)?; let order_risk_decreasing = is_spot_order_risk_decreasing(&user.orders[new_order_index], &balance_type, token_amount)?; diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 8541bfbca..9212c7e51 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -32,6 +32,7 @@ use crate::state::user::{PerpPosition, SpotPosition, User}; use num_integer::Roots; use solana_program::msg; use std::cmp::{max, min, Ordering}; +use std::ops::Neg; #[cfg(test)] mod tests; @@ -332,11 +333,21 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( } } } else { + let signed_token_amount = spot_position.get_signed_token_amount(&spot_market)?; let (worst_case_token_amount, worst_cast_quote_token_amount): (i128, i128) = spot_position.get_worst_case_token_amounts( &spot_market, oracle_price_data, - None, + if strict { + Some( + spot_market + .historical_oracle_data + .last_oracle_price_twap_5min, + ) + } else { + None + }, + Some(signed_token_amount), )?; if worst_case_token_amount == 0 { @@ -349,9 +360,9 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( )?; } - let worst_case_token_value = if strict { + let signed_token_value = if strict { get_strict_token_value( - worst_case_token_amount, + signed_token_amount, spot_market.decimals, oracle_price_data, spot_market @@ -360,12 +371,16 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( )? } else { get_token_value( - worst_case_token_amount, + signed_token_amount, spot_market.decimals, oracle_price_data.price, )? }; + // the worst case token value is the deposit/borrow amount * oracle + worst case order size * oracle + let worst_case_token_value = + signed_token_value.safe_add(worst_cast_quote_token_amount.neg())?; + margin_requirement = margin_requirement.safe_add(spot_position.margin_requirement_for_open_orders()?)?; diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index 5e901d548..acf8c0449 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -1,24 +1,37 @@ use std::cmp::min; +use std::ops::{Neg, Sub}; use solana_program::msg; use crate::controller::position::PositionDelta; use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; -use crate::math; use crate::math::amm::calculate_amm_available_liquidity; use crate::math::auction::is_auction_complete; use crate::math::casting::Cast; +use crate::{ + math, BASE_PRECISION_I128, OPEN_ORDER_MARGIN_REQUIREMENT, PRICE_PRECISION_I128, + QUOTE_PRECISION_I128, SPOT_WEIGHT_PRECISION, +}; use crate::math::constants::MARGIN_PRECISION_U128; +use crate::math::margin::{ + calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, +}; use crate::math::position::calculate_entry_price; use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::{get_strict_token_value, get_token_value}; use crate::math::spot_withdraw::calculate_availability_borrow_liquidity; use crate::math_error; use crate::print_error; +use crate::state::oracle_map::OracleMap; use crate::state::perp_market::PerpMarket; +use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; -use crate::state::user::{MarketType, Order, OrderStatus, OrderTriggerCondition, User}; +use crate::state::spot_market_map::SpotMarketMap; +use crate::state::user::{ + MarketType, Order, OrderStatus, OrderTriggerCondition, PerpPosition, User, +}; use crate::validate; #[cfg(test)] @@ -660,3 +673,252 @@ pub fn find_maker_orders( Ok(orders) } + +pub fn calculate_max_perp_order_size( + user: &User, + position_index: usize, + market_index: u16, + direction: PositionDirection, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, +) -> DriftResult { + // calculate initial margin requirement + let (margin_requirement, total_collateral, _, _, _, _) = + calculate_margin_requirement_and_total_collateral_and_liability_info( + user, + perp_market_map, + MarginRequirementType::Initial, + spot_market_map, + oracle_map, + None, + true, + )?; + + let free_collateral = total_collateral.safe_sub(margin_requirement.cast()?)?; + + let perp_market = perp_market_map.get_ref(&market_index)?; + + let oracle_price_data = oracle_map.get_price_data(&perp_market.amm.oracle)?; + + let perp_position: &PerpPosition = &user.perp_positions[position_index]; + let base_asset_amount = perp_position.base_asset_amount; + let worst_case_base_asset_amount = perp_position.worst_case_base_asset_amount()?; + + let margin_ratio = perp_market.get_margin_ratio( + worst_case_base_asset_amount.unsigned_abs(), + MarginRequirementType::Initial, + )?; + + let mut order_size = 0_u64; + // account for order flipping worst case base asset amount + if worst_case_base_asset_amount < 0 && direction == PositionDirection::Long { + order_size = worst_case_base_asset_amount + .abs() + .cast::()? + .safe_sub(base_asset_amount.safe_add(perp_position.open_bids)?)? + .unsigned_abs(); + } else if worst_case_base_asset_amount > 0 && direction == PositionDirection::Short { + order_size = worst_case_base_asset_amount + .neg() + .cast::()? + .safe_sub(base_asset_amount.safe_add(perp_position.open_asks)?)? + .unsigned_abs(); + } + + if free_collateral <= 0 { + let max_risk_reducing_order_size = base_asset_amount.safe_mul(2)?.unsigned_abs(); + return standardize_base_asset_amount( + order_size.min(max_risk_reducing_order_size), + perp_market.amm.order_step_size, + ); + } + + let order_size = free_collateral + .safe_sub(OPEN_ORDER_MARGIN_REQUIREMENT.cast()?)? + .safe_mul(BASE_PRECISION_I128 / QUOTE_PRECISION_I128)? + .safe_mul(MARGIN_PRECISION_U128.cast()?)? + .safe_div(margin_ratio.cast()?)? + .safe_mul(PRICE_PRECISION_I128)? + .safe_div(oracle_price_data.price.cast()?)? + .cast::()? + .safe_add(order_size)?; + + standardize_base_asset_amount(order_size, perp_market.amm.order_step_size) +} + +pub fn calculate_max_spot_order_size( + user: &User, + market_index: u16, + direction: PositionDirection, + perp_market_map: &PerpMarketMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, +) -> DriftResult { + // calculate initial margin requirement + let (margin_requirement, total_collateral, _, _, _, _) = + calculate_margin_requirement_and_total_collateral_and_liability_info( + user, + perp_market_map, + MarginRequirementType::Initial, + spot_market_map, + oracle_map, + None, + true, + )?; + + let mut order_size = 0_u64; + let mut free_collateral = total_collateral.safe_sub(margin_requirement.cast()?)?; + + let spot_market = spot_market_map.get_ref(&market_index)?; + + let oracle_price_data = oracle_map.get_price_data(&spot_market.oracle)?; + let twap = spot_market + .historical_oracle_data + .last_oracle_price_twap_5min; + let max_oracle_price = oracle_price_data.price.max(twap); + + let spot_position = user.get_spot_position(market_index)?; + let signed_token_amount = spot_position.get_signed_token_amount(&spot_market)?; + let (worst_case_token_amount, worst_case_quote_amount) = spot_position + .get_worst_case_token_amounts( + &spot_market, + oracle_price_data, + Some(twap), + Some(signed_token_amount), + )?; + + let token_value_before = get_strict_token_value( + signed_token_amount, + spot_market.decimals, + oracle_price_data, + twap, + )?; + + let worst_case_token_value_before = + token_value_before.safe_add(worst_case_quote_amount.neg())?; + + // account for order flipping worst case base asset amount + if worst_case_token_amount < 0 && direction == PositionDirection::Long { + // first figure out how much free collateral existing positions/orders consumed + let liability_weight = spot_market.get_liability_weight( + worst_case_token_amount.unsigned_abs(), + &MarginRequirementType::Initial, + )?; + + let free_collateral_consumption_before = worst_case_quote_amount.safe_add( + worst_case_token_value_before + .safe_mul(liability_weight.cast()?)? + .safe_div(SPOT_WEIGHT_PRECISION.cast()?)?, + )?; + + // then calculate the free collateral consumed by placing order to flip worst case token amount + + // e.g. worst case: -15, signed token amount: 2, open bids: 5 + // then bids_to_flip = 15 - (2 + 5) = 8 + let bids_to_flip = worst_case_token_amount + .abs() + .safe_sub(signed_token_amount.safe_add(spot_position.open_bids.cast()?)?)?; + + let worst_case_quote_amount_after = -get_token_value( + spot_position + .open_bids + .cast::()? + .safe_add(bids_to_flip)?, + spot_market.decimals, + max_oracle_price, + )?; + + let worst_case_token_value_after = + token_value_before.safe_add(worst_case_quote_amount_after.neg())?; + + let asset_weight = spot_market.get_asset_weight( + worst_case_token_amount.unsigned_abs(), + &MarginRequirementType::Initial, + )?; + + let free_collateral_consumption_after = worst_case_token_value_after + .safe_mul(asset_weight.cast()?)? + .safe_div(SPOT_WEIGHT_PRECISION.cast()?)? + .safe_add(worst_case_quote_amount_after)?; + + free_collateral = free_collateral.safe_add( + free_collateral_consumption_after.safe_sub(free_collateral_consumption_before)?, + )?; + + order_size = bids_to_flip.cast()?; + } else if worst_case_token_amount > 0 && direction == PositionDirection::Short { + let asset_weight = spot_market.get_asset_weight( + worst_case_token_amount.unsigned_abs(), + &MarginRequirementType::Initial, + )?; + + let free_collateral_contribution_before = worst_case_token_value_before + .safe_mul(asset_weight.cast()?)? + .safe_div(SPOT_WEIGHT_PRECISION.cast()?)? + .safe_add(worst_case_quote_amount)?; + + let asks_to_flip = worst_case_token_amount + .neg() + .safe_sub(signed_token_amount.safe_add(spot_position.open_asks.cast()?)?)?; + + let worst_case_quote_amount_after = -get_token_value( + spot_position + .open_asks + .cast::()? + .safe_add(asks_to_flip)?, + spot_market.decimals, + max_oracle_price, + )?; + + let worst_case_token_value_after = + token_value_before.safe_add(worst_case_quote_amount_after.neg())?; + + let liability_weight = spot_market.get_liability_weight( + worst_case_token_amount.unsigned_abs(), + &MarginRequirementType::Initial, + )?; + + let free_collateral_contribution_after = worst_case_quote_amount_after.safe_add( + worst_case_token_value_after + .safe_mul(liability_weight.cast()?)? + .safe_div(SPOT_WEIGHT_PRECISION.cast()?)?, + )?; + + free_collateral = free_collateral.safe_add( + free_collateral_contribution_after.safe_sub(free_collateral_contribution_before)?, + )?; + + order_size = asks_to_flip.abs().cast()?; + } + + if free_collateral <= 0 { + let max_risk_reducing_order_size = signed_token_amount.safe_mul(2)?.abs().cast::()?; + return standardize_base_asset_amount( + order_size.min(max_risk_reducing_order_size), + spot_market.order_step_size, + ); + } + + let free_collateral_delta = if direction == PositionDirection::Long { + SPOT_WEIGHT_PRECISION.sub(spot_market.initial_asset_weight) + } else { + spot_market + .initial_liability_weight + .sub(SPOT_WEIGHT_PRECISION) + }; + + let precision_increase = 10i128.pow(spot_market.decimals - 6); + + let order_size = free_collateral + .safe_sub(OPEN_ORDER_MARGIN_REQUIREMENT.cast()?)? + .safe_mul(precision_increase)? + .safe_mul(SPOT_WEIGHT_PRECISION.cast()?)? + .safe_div(free_collateral_delta.cast()?)? + .safe_mul(PRICE_PRECISION_I128)? + .safe_div(max_oracle_price.cast()?)? + .cast::()? + .safe_add(order_size)?; + + standardize_base_asset_amount(order_size, spot_market.order_step_size) +} diff --git a/programs/drift/src/math/orders/tests.rs b/programs/drift/src/math/orders/tests.rs index f6dadc86e..38fe080f9 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -1426,3 +1426,881 @@ mod find_maker_orders { assert_eq!(orders, expected_orders); } } + +mod calculate_max_spot_order_size { + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::math::constants::{ + LIQUIDATION_FEE_PRECISION, PRICE_PRECISION_I64, SPOT_BALANCE_PRECISION, + SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, + }; + use crate::math::margin::{ + calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, + }; + use crate::math::orders::calculate_max_spot_order_size; + use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::oracle_map::OracleMap; + + use crate::create_anchor_account_info; + use crate::state::perp_market_map::PerpMarketMap; + use crate::state::spot_market::{SpotBalanceType, SpotMarket}; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{Order, PerpPosition, SpotPosition, User}; + use crate::test_utils::get_pyth_price; + use crate::test_utils::*; + use crate::{create_account_info, PositionDirection}; + + #[test] + pub fn usdc_deposit_and_5x_sol_bid() { + let slot = 0_u64; + + let mut sol_oracle_price = get_pyth_price(100, 6); + let sol_oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + sol_oracle_price, + &sol_oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let _market_map = PerpMarketMap::empty(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_spot_market = SpotMarket { + market_index: 1, + oracle_source: OracleSource::Pyth, + oracle: sol_oracle_price_key, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + initial_asset_weight: 8 * SPOT_WEIGHT_PRECISION / 10, + maintenance_asset_weight: 9 * SPOT_WEIGHT_PRECISION / 10, + initial_liability_weight: 12 * SPOT_WEIGHT_PRECISION / 10, + maintenance_liability_weight: 11 * SPOT_WEIGHT_PRECISION / 10, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap_5min: 110 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + let spot_market_account_infos = Vec::from([ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + ]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + spot_positions[1] = SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Deposit, + ..SpotPosition::default() + }; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: [PerpPosition::default(); 8], + spot_positions, + ..User::default() + }; + + let max_order_size = calculate_max_spot_order_size( + &user, + 1, + PositionDirection::Long, + &PerpMarketMap::empty(), + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + assert_eq!(max_order_size, 454545000000); + + user.spot_positions[1].open_orders = 1; + user.spot_positions[1].open_bids = max_order_size as i64; + + let (margin_requirement, total_collateral, _, _, _, _) = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &PerpMarketMap::empty(), + MarginRequirementType::Initial, + &spot_market_map, + &mut oracle_map, + None, + true, + ) + .unwrap(); + + assert_eq!(total_collateral.unsigned_abs(), margin_requirement); + } + + #[test] + pub fn usdc_deposit_and_5x_sol_bid_already_short() { + let slot = 0_u64; + + let mut sol_oracle_price = get_pyth_price(100, 6); + let sol_oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + sol_oracle_price, + &sol_oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let _market_map = PerpMarketMap::empty(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_spot_market = SpotMarket { + market_index: 1, + oracle_source: OracleSource::Pyth, + oracle: sol_oracle_price_key, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + initial_asset_weight: 8 * SPOT_WEIGHT_PRECISION / 10, + maintenance_asset_weight: 9 * SPOT_WEIGHT_PRECISION / 10, + initial_liability_weight: 12 * SPOT_WEIGHT_PRECISION / 10, + maintenance_liability_weight: 11 * SPOT_WEIGHT_PRECISION / 10, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap_5min: 110 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + let spot_market_account_infos = Vec::from([ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + ]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + spot_positions[1] = SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Borrow, + scaled_balance: 500 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let user = User { + orders: [Order::default(); 32], + perp_positions: [PerpPosition::default(); 8], + spot_positions, + ..User::default() + }; + + let max_order_size = calculate_max_spot_order_size( + &user, + 1, + PositionDirection::Long, + &PerpMarketMap::empty(), + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + assert_eq!(max_order_size, 1000000000000); + } + + #[test] + pub fn usdc_deposit_and_5x_sol_sell() { + let slot = 0_u64; + + let mut sol_oracle_price = get_pyth_price(100, 6); + let sol_oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + sol_oracle_price, + &sol_oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let _market_map = PerpMarketMap::empty(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_spot_market = SpotMarket { + market_index: 1, + oracle_source: OracleSource::Pyth, + oracle: sol_oracle_price_key, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + initial_asset_weight: 8 * SPOT_WEIGHT_PRECISION / 10, + maintenance_asset_weight: 9 * SPOT_WEIGHT_PRECISION / 10, + initial_liability_weight: 12 * SPOT_WEIGHT_PRECISION / 10, + maintenance_liability_weight: 11 * SPOT_WEIGHT_PRECISION / 10, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap_5min: 110 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + let spot_market_account_infos = Vec::from([ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + ]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + spot_positions[1] = SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Deposit, + ..SpotPosition::default() + }; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: [PerpPosition::default(); 8], + spot_positions, + ..User::default() + }; + + let max_order_size = calculate_max_spot_order_size( + &user, + 1, + PositionDirection::Short, + &PerpMarketMap::empty(), + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + assert_eq!(max_order_size, 454545000000); + + user.spot_positions[1].open_orders = 1; + user.spot_positions[1].open_asks = -(max_order_size as i64); + + let (margin_requirement, total_collateral, _, _, _, _) = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &PerpMarketMap::empty(), + MarginRequirementType::Initial, + &spot_market_map, + &mut oracle_map, + None, + true, + ) + .unwrap(); + + assert_eq!(total_collateral.unsigned_abs(), margin_requirement); + } + + #[test] + pub fn usdc_deposit_and_5x_sol_sell_already_long() { + let slot = 0_u64; + + let mut sol_oracle_price = get_pyth_price(100, 6); + let sol_oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + sol_oracle_price, + &sol_oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let _market_map = PerpMarketMap::empty(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let mut sol_spot_market = SpotMarket { + market_index: 1, + oracle_source: OracleSource::Pyth, + oracle: sol_oracle_price_key, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + cumulative_borrow_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + initial_asset_weight: 8 * SPOT_WEIGHT_PRECISION / 10, + maintenance_asset_weight: 9 * SPOT_WEIGHT_PRECISION / 10, + initial_liability_weight: 12 * SPOT_WEIGHT_PRECISION / 10, + maintenance_liability_weight: 11 * SPOT_WEIGHT_PRECISION / 10, + liquidator_fee: LIQUIDATION_FEE_PRECISION / 1000, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap_5min: 110 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + let spot_market_account_infos = Vec::from([ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + ]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + spot_positions[1] = SpotPosition { + market_index: 1, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 500 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let user = User { + orders: [Order::default(); 32], + perp_positions: [PerpPosition::default(); 8], + spot_positions, + ..User::default() + }; + + let max_order_size = calculate_max_spot_order_size( + &user, + 1, + PositionDirection::Short, + &PerpMarketMap::empty(), + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + assert_eq!(max_order_size, 3181817727272); + } +} + +mod calculate_max_perp_order_size { + use std::str::FromStr; + + use anchor_lang::Owner; + use solana_program::pubkey::Pubkey; + + use crate::math::constants::{ + SPOT_BALANCE_PRECISION, SPOT_BALANCE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, + SPOT_WEIGHT_PRECISION, + }; + use crate::math::margin::{ + calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, + }; + use crate::math::orders::calculate_max_perp_order_size; + use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::oracle_map::OracleMap; + + use crate::state::perp_market::{PerpMarket, AMM}; + use crate::state::perp_market_map::PerpMarketMap; + use crate::state::spot_market::{SpotBalanceType, SpotMarket}; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::user::{Order, PerpPosition, SpotPosition, User}; + use crate::test_utils::get_pyth_price; + use crate::test_utils::*; + use crate::{create_account_info, PositionDirection}; + use crate::{ + create_anchor_account_info, MarketStatus, AMM_RESERVE_PRECISION, PEG_PRECISION, + PRICE_PRECISION, + }; + + #[test] + pub fn sol_perp_5x_bid() { + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 1000, + order_tick_size: 1, + oracle: oracle_price_key, + base_spread: 0, // 1 basis point + historical_oracle_data: HistoricalOracleData { + last_oracle_price: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, + + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 2000, + margin_ratio_maintenance: 1000, + status: MarketStatus::Initialized, + ..PerpMarket::default_test() + }; + market.amm.max_base_asset_reserve = u128::MAX; + market.amm.min_base_asset_reserve = 0; + + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let spot_market_account_infos = Vec::from([&usdc_spot_market_account_info]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Long, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + assert_eq!(max_order_size, 499999500000); + + user.perp_positions[0].open_orders = 1; + user.perp_positions[0].open_bids = max_order_size as i64; + + let (margin_requirement, total_collateral, _, _, _, _) = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &market_map, + MarginRequirementType::Initial, + &spot_market_map, + &mut oracle_map, + None, + true, + ) + .unwrap(); + + assert_eq!(total_collateral.unsigned_abs(), margin_requirement); + } + + #[test] + pub fn sol_perp_5x_bid_when_short_5x() { + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 1000, + order_tick_size: 1, + oracle: oracle_price_key, + base_spread: 0, // 1 basis point + historical_oracle_data: HistoricalOracleData { + last_oracle_price: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, + + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 2000, + margin_ratio_maintenance: 1000, + status: MarketStatus::Initialized, + ..PerpMarket::default_test() + }; + market.amm.max_base_asset_reserve = u128::MAX; + market.amm.min_base_asset_reserve = 0; + + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let spot_market_account_infos = Vec::from([&usdc_spot_market_account_info]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: -500000000000, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Long, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + assert_eq!(max_order_size, 1000000000000); + } + + #[test] + pub fn sol_perp_5x_ask() { + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 1000, + order_tick_size: 1, + oracle: oracle_price_key, + base_spread: 0, // 1 basis point + historical_oracle_data: HistoricalOracleData { + last_oracle_price: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, + + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 2000, + margin_ratio_maintenance: 1000, + status: MarketStatus::Initialized, + ..PerpMarket::default_test() + }; + market.amm.max_base_asset_reserve = u128::MAX; + market.amm.min_base_asset_reserve = 0; + + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let spot_market_account_infos = Vec::from([&usdc_spot_market_account_info]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Short, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + assert_eq!(max_order_size, 499999500000); + + user.perp_positions[0].open_orders = 1; + user.perp_positions[0].open_asks = -(max_order_size as i64); + + let (margin_requirement, total_collateral, _, _, _, _) = + calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &market_map, + MarginRequirementType::Initial, + &spot_market_map, + &mut oracle_map, + None, + true, + ) + .unwrap(); + + assert_eq!(total_collateral.unsigned_abs(), margin_requirement); + } + + #[test] + pub fn sol_perp_5x_ask_when_long_5x() { + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 1000, + order_tick_size: 1, + oracle: oracle_price_key, + base_spread: 0, // 1 basis point + historical_oracle_data: HistoricalOracleData { + last_oracle_price: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, + + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 2000, + margin_ratio_maintenance: 1000, + status: MarketStatus::Initialized, + ..PerpMarket::default_test() + }; + market.amm.max_base_asset_reserve = u128::MAX; + market.amm.min_base_asset_reserve = 0; + + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let spot_market_account_infos = Vec::from([&usdc_spot_market_account_info]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + base_asset_amount: 500000000000, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Short, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + assert_eq!(max_order_size, 1000000000000); + } +} diff --git a/programs/drift/src/state/spot_market.rs b/programs/drift/src/state/spot_market.rs index 06160672c..1dc6bdad0 100644 --- a/programs/drift/src/state/spot_market.rs +++ b/programs/drift/src/state/spot_market.rs @@ -102,7 +102,7 @@ impl Default for SpotMarket { last_interest_ts: 0, last_twap_ts: 0, expiry_ts: 0, - order_step_size: 0, + order_step_size: 1, order_tick_size: 0, min_order_size: 0, max_position_size: 0, diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index d0f48b636..46c5ee6fb 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -367,6 +367,7 @@ impl SpotPosition { &self, spot_market: &SpotMarket, oracle_price_data: &OraclePriceData, + twap_5min: Option, token_amount: Option, ) -> DriftResult<(i128, i128)> { let token_amount = match token_amount { @@ -378,19 +379,18 @@ impl SpotPosition { let token_amount_all_asks_fill = token_amount.safe_add(self.open_asks as i128)?; + let oracle_price = match twap_5min { + Some(twap_5min) => twap_5min.max(oracle_price_data.price), + None => oracle_price_data.price, + }; + if token_amount_all_bids_fill.abs() > token_amount_all_asks_fill.abs() { - let worst_case_quote_token_amount = get_token_value( - -self.open_bids as i128, - spot_market.decimals, - oracle_price_data.price, - )?; + let worst_case_quote_token_amount = + get_token_value(-self.open_bids as i128, spot_market.decimals, oracle_price)?; Ok((token_amount_all_bids_fill, worst_case_quote_token_amount)) } else { - let worst_case_quote_token_amount = get_token_value( - -self.open_asks as i128, - spot_market.decimals, - oracle_price_data.price, - )?; + let worst_case_quote_token_amount = + get_token_value(-self.open_asks as i128, spot_market.decimals, oracle_price)?; Ok((token_amount_all_asks_fill, worst_case_quote_token_amount)) } } diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index 2dfeb973c..65d62c591 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -630,7 +630,7 @@ mod get_worst_case_token_amounts { }; let (worst_case_token_amount, worst_case_quote_token_amount) = spot_position - .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None) + .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None, None) .unwrap(); assert_eq!(worst_case_token_amount, 10_i128.pow(9)); @@ -665,7 +665,7 @@ mod get_worst_case_token_amounts { }; let (worst_case_token_amount, worst_case_quote_token_amount) = spot_position - .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None) + .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None, None) .unwrap(); assert_eq!(worst_case_token_amount, -(10_i128.pow(9))); @@ -700,7 +700,7 @@ mod get_worst_case_token_amounts { }; let (worst_case_token_amount, worst_case_quote_token_amount) = spot_position - .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None) + .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None, None) .unwrap(); assert_eq!(worst_case_token_amount, 2 * 10_i128.pow(9)); @@ -735,7 +735,7 @@ mod get_worst_case_token_amounts { }; let (worst_case_token_amount, worst_case_quote_token_amount) = spot_position - .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None) + .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None, None) .unwrap(); assert_eq!(worst_case_token_amount, -(10_i128.pow(9))); @@ -770,7 +770,7 @@ mod get_worst_case_token_amounts { }; let (worst_case_token_amount, worst_case_quote_token_amount) = spot_position - .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None) + .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None, None) .unwrap(); assert_eq!(worst_case_token_amount, 3 * 10_i128.pow(9)); @@ -806,7 +806,7 @@ mod get_worst_case_token_amounts { }; let (worst_case_token_amount, worst_case_quote_token_amount) = spot_position - .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None) + .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None, None) .unwrap(); assert_eq!(worst_case_token_amount, -2 * 10_i128.pow(9)); @@ -842,7 +842,7 @@ mod get_worst_case_token_amounts { }; let (worst_case_token_amount, worst_case_quote_token_amount) = spot_position - .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None) + .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None, None) .unwrap(); assert_eq!(worst_case_token_amount, 3 * 10_i128.pow(9)); @@ -878,7 +878,7 @@ mod get_worst_case_token_amounts { }; let (worst_case_token_amount, worst_case_quote_token_amount) = spot_position - .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None) + .get_worst_case_token_amounts(&spot_market, &oracle_price_data, None, None) .unwrap(); assert_eq!(worst_case_token_amount, -3 * 10_i128.pow(9)); diff --git a/sdk/src/constants/numericConstants.ts b/sdk/src/constants/numericConstants.ts index 6e18fb493..c827612a9 100644 --- a/sdk/src/constants/numericConstants.ts +++ b/sdk/src/constants/numericConstants.ts @@ -17,6 +17,7 @@ export const BN_MAX = new BN(Number.MAX_SAFE_INTEGER); export const TEN_MILLION = TEN_THOUSAND.mul(TEN_THOUSAND); export const MAX_LEVERAGE = new BN(5); +export const MAX_LEVERAGE_ORDER_SIZE = new BN('18446744073709551615'); export const PERCENTAGE_PRECISION_EXP = new BN(6); export const PERCENTAGE_PRECISION = new BN(10).pow(PERCENTAGE_PRECISION_EXP); diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index 367e16baf..a4f4aed2c 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -5,6 +5,7 @@ if [ "$1" != "--skip-build" ] fi test_files=( + maxLeverageOrderParams.ts multipleMakerOrders.ts postOnlyAmmFulfillment.ts imbalancePerpPnl.ts diff --git a/tests/maxLeverageOrderParams.ts b/tests/maxLeverageOrderParams.ts new file mode 100644 index 000000000..c88bbb506 --- /dev/null +++ b/tests/maxLeverageOrderParams.ts @@ -0,0 +1,214 @@ +import * as anchor from '@project-serum/anchor'; +import { assert } from 'chai'; + +import { Program } from '@project-serum/anchor'; + +import { PublicKey } from '@solana/web3.js'; + +import { + BN, + OracleSource, + TestClient, + PRICE_PRECISION, + PositionDirection, + EventSubscriber, + OracleGuardRails, + MarketStatus, + LIQUIDATION_PCT_PRECISION, +} from '../sdk/src'; + +import { + mockOracle, + mockUSDCMint, + mockUserUSDCAccount, + initializeQuoteSpotMarket, + createUserWithUSDCAndWSOLAccount, + initializeSolSpotMarket, +} from './testHelpers'; +import { + BulkAccountLoader, + getMarketOrderParams, + MAX_LEVERAGE_ORDER_SIZE, +} from '../sdk'; + +describe('max leverage order params', () => { + const provider = anchor.AnchorProvider.local(undefined, { + preflightCommitment: 'confirmed', + commitment: 'confirmed', + }); + const connection = provider.connection; + anchor.setProvider(provider); + const chProgram = anchor.workspace.Drift as Program; + + let driftClient: TestClient; + const eventSubscriber = new EventSubscriber(connection, chProgram, { + commitment: 'recent', + }); + eventSubscriber.subscribe(); + + const bulkAccountLoader = new BulkAccountLoader(connection, 'confirmed', 1); + + let usdcMint; + let userUSDCAccount; + + let lendorDriftClient: TestClient; + let lendorDriftClientWSOLAccount: PublicKey; + let lendorDriftClientUSDCAccount: PublicKey; + + let solOracle: PublicKey; + + // ammInvariant == k == x * y + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(5 * 10 ** 13).mul( + mantissaSqrtScale + ); + + const usdcAmount = new BN(10 * 10 ** 6); + + before(async () => { + usdcMint = await mockUSDCMint(provider); + userUSDCAccount = await mockUserUSDCAccount(usdcMint, usdcAmount, provider); + + solOracle = await mockOracle(1); + + driftClient = new TestClient({ + connection, + wallet: provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: [0], + spotMarketIndexes: [0, 1], + oracleInfos: [ + { + publicKey: solOracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + + await driftClient.initialize(usdcMint.publicKey, true); + await driftClient.subscribe(); + + await driftClient.updateInitialPctToLiquidate( + LIQUIDATION_PCT_PRECISION.toNumber() + ); + + await initializeQuoteSpotMarket(driftClient, usdcMint.publicKey); + await initializeSolSpotMarket(driftClient, solOracle); + await driftClient.updatePerpAuctionDuration(new BN(0)); + + const periodicity = new BN(0); + + await driftClient.initializePerpMarket( + solOracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity + ); + await driftClient.updatePerpMarketStatus(0, MarketStatus.ACTIVE); + + await driftClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + userUSDCAccount.publicKey + ); + + const oracleGuardRails: OracleGuardRails = { + priceDivergence: { + markOracleDivergenceNumerator: new BN(1), + markOracleDivergenceDenominator: new BN(10), + }, + validity: { + slotsBeforeStaleForAmm: new BN(100), + slotsBeforeStaleForMargin: new BN(100), + confidenceIntervalMaxSize: new BN(100000), + tooVolatileRatio: new BN(55), // allow 55x change + }, + }; + + await driftClient.updateOracleGuardRails(oracleGuardRails); + + const lenderSolAmount = new BN(100 * 10 ** 9); + const lenderUSDCAmount = usdcAmount.mul(new BN(100)); + [ + lendorDriftClient, + lendorDriftClientWSOLAccount, + lendorDriftClientUSDCAccount, + ] = await createUserWithUSDCAndWSOLAccount( + provider, + usdcMint, + chProgram, + lenderSolAmount, + lenderUSDCAmount, + [0], + [0, 1], + [ + { + publicKey: solOracle, + source: OracleSource.PYTH, + }, + ], + bulkAccountLoader + ); + await lendorDriftClient.subscribe(); + + const spotMarketIndex = 1; + await lendorDriftClient.deposit( + lenderSolAmount, + spotMarketIndex, + lendorDriftClientWSOLAccount + ); + + await lendorDriftClient.deposit( + lenderUSDCAmount, + 0, + lendorDriftClientUSDCAccount + ); + }); + + after(async () => { + await driftClient.unsubscribe(); + await lendorDriftClient.unsubscribe(); + await eventSubscriber.unsubscribe(); + }); + + it('max perp leverage', async () => { + await driftClient.placePerpOrder( + getMarketOrderParams({ + direction: PositionDirection.LONG, + marketIndex: 0, + baseAssetAmount: MAX_LEVERAGE_ORDER_SIZE, + userOrderId: 1, + }) + ); + + let leverage = driftClient.getUser().getLeverage().toNumber() / 10000; + assert(leverage === 4.995); + + await driftClient.cancelOrderByUserId(1); + + // test placing order with short direction + await driftClient.placePerpOrder( + getMarketOrderParams({ + direction: PositionDirection.SHORT, + marketIndex: 0, + baseAssetAmount: MAX_LEVERAGE_ORDER_SIZE, + userOrderId: 1, + }) + ); + + leverage = driftClient.getUser().getLeverage().toNumber() / 10000; + assert(leverage === 4.995); + + await driftClient.cancelOrderByUserId(1); + }); +});