497 lines
17 KiB
Rust
497 lines
17 KiB
Rust
use std::mem::size_of;
|
|
|
|
use anchor_lang::prelude::*;
|
|
use fixed::types::I80F48;
|
|
|
|
use static_assertions::const_assert_eq;
|
|
|
|
use crate::accounts_zerocopy::KeyedAccountReader;
|
|
use crate::error::MangoError;
|
|
use crate::logs::PerpUpdateFundingLogV2;
|
|
use crate::state::orderbook::Side;
|
|
use crate::state::{oracle, TokenIndex};
|
|
|
|
use super::{orderbook, OracleConfig, OracleState, Orderbook, StablePriceModel, DAY_I80F48};
|
|
|
|
pub type PerpMarketIndex = u16;
|
|
|
|
#[account(zero_copy)]
|
|
#[derive(Debug)]
|
|
pub struct PerpMarket {
|
|
// ABI: Clients rely on this being at offset 8
|
|
pub group: Pubkey,
|
|
|
|
/// Token index that settlements happen in.
|
|
///
|
|
/// Currently required to be 0, USDC. In the future settlement
|
|
/// may be allowed to happen in other tokens.
|
|
pub settle_token_index: TokenIndex,
|
|
|
|
/// Index of this perp market. Other data, like the MangoAccount's PerpPosition
|
|
/// reference this market via this index. Unique for this group's perp markets.
|
|
pub perp_market_index: PerpMarketIndex,
|
|
|
|
/// Field used to contain the trusted_market flag and is now unused.
|
|
pub blocked1: u8,
|
|
|
|
/// Is this market covered by the group insurance fund?
|
|
pub group_insurance_fund: u8,
|
|
|
|
/// PDA bump
|
|
pub bump: u8,
|
|
|
|
/// Number of decimals used for the base token.
|
|
///
|
|
/// Used to convert the oracle's price into a native/native price.
|
|
pub base_decimals: u8,
|
|
|
|
/// Name. Trailing zero bytes are ignored.
|
|
pub name: [u8; 16],
|
|
|
|
/// Address of the BookSide account for bids
|
|
pub bids: Pubkey,
|
|
/// Address of the BookSide account for asks
|
|
pub asks: Pubkey,
|
|
/// Address of the EventQueue account
|
|
pub event_queue: Pubkey,
|
|
|
|
/// Oracle account address
|
|
pub oracle: Pubkey,
|
|
/// Oracle configuration
|
|
pub oracle_config: OracleConfig,
|
|
/// Maintains a stable price based on the oracle price that is less volatile.
|
|
pub stable_price_model: StablePriceModel,
|
|
|
|
/// Number of quote native in a quote lot. Must be a power of 10.
|
|
///
|
|
/// Primarily useful for increasing the tick size on the market: A lot price
|
|
/// of 1 becomes a native price of quote_lot_size/base_lot_size becomes a
|
|
/// ui price of quote_lot_size*base_decimals/base_lot_size/quote_decimals.
|
|
pub quote_lot_size: i64,
|
|
|
|
/// Number of base native in a base lot. Must be a power of 10.
|
|
///
|
|
/// Example: If base decimals for the underlying asset is 6, base lot size
|
|
/// is 100 and and base position lots is 10_000 then base position native is
|
|
/// 1_000_000 and base position ui is 1.
|
|
pub base_lot_size: i64,
|
|
|
|
/// These weights apply to the base position. The quote position has
|
|
/// no explicit weight (but may be covered by the overall pnl asset weight).
|
|
pub maint_base_asset_weight: I80F48,
|
|
pub init_base_asset_weight: I80F48,
|
|
pub maint_base_liab_weight: I80F48,
|
|
pub init_base_liab_weight: I80F48,
|
|
|
|
/// Number of base lot pairs currently active in the market. Always >= 0.
|
|
pub open_interest: i64,
|
|
|
|
/// Total number of orders seen
|
|
pub seq_num: u64,
|
|
|
|
/// Timestamp in seconds that the market was registered at.
|
|
pub registration_time: u64,
|
|
|
|
// Funding
|
|
/// Minimal funding rate per day, must be <= 0.
|
|
pub min_funding: I80F48,
|
|
/// Maximal funding rate per day, must be >= 0.
|
|
pub max_funding: I80F48,
|
|
/// For funding, get the impact price this many base lots deep into the book.
|
|
pub impact_quantity: i64,
|
|
|
|
/// Current long funding value. Increasing it means that every long base lot
|
|
/// needs to pay that amount of quote native in funding.
|
|
///
|
|
/// PerpPosition uses and tracks it settle funding. Updated by the perp
|
|
/// keeper instruction.
|
|
pub long_funding: I80F48,
|
|
/// See long_funding.
|
|
pub short_funding: I80F48,
|
|
/// timestamp that funding was last updated in
|
|
pub funding_last_updated: u64,
|
|
|
|
/// Fees
|
|
|
|
/// Fee for base position liquidation
|
|
pub base_liquidation_fee: I80F48,
|
|
/// Fee when matching maker orders. May be negative.
|
|
pub maker_fee: I80F48,
|
|
/// Fee for taker orders, may not be negative.
|
|
pub taker_fee: I80F48,
|
|
|
|
/// Fees accrued in native quote currency
|
|
pub fees_accrued: I80F48,
|
|
/// Fees settled in native quote currency
|
|
pub fees_settled: I80F48,
|
|
|
|
/// Fee (in quote native) to charge for ioc orders
|
|
pub fee_penalty: f32,
|
|
|
|
// Settling incentives
|
|
/// In native units of settlement token, given to each settle call above the
|
|
/// settle_fee_amount_threshold.
|
|
pub settle_fee_flat: f32,
|
|
/// Pnl settlement amount needed to be eligible for the flat fee.
|
|
pub settle_fee_amount_threshold: f32,
|
|
/// Fraction of pnl to pay out as fee if +pnl account has low health.
|
|
pub settle_fee_fraction_low_health: f32,
|
|
|
|
// Pnl settling limits
|
|
/// Controls the strictness of the settle limit.
|
|
/// Set to a negative value to disable the limit.
|
|
///
|
|
/// This factor applies to the settle limit in two ways
|
|
/// - for the unrealized pnl settle limit, the factor is multiplied with the stable perp base value
|
|
/// (i.e. limit_factor * base_native * stable_price)
|
|
/// - when increasing the realized pnl settle limit (stored per PerpPosition), the factor is
|
|
/// multiplied with the stable value of the perp pnl being realized
|
|
/// (i.e. limit_factor * reduced_native * stable_price)
|
|
///
|
|
/// See also PerpPosition::settle_pnl_limit_realized_trade
|
|
pub settle_pnl_limit_factor: f32,
|
|
pub padding3: [u8; 4],
|
|
/// Window size in seconds for the perp settlement limit
|
|
pub settle_pnl_limit_window_size_ts: u64,
|
|
|
|
/// If true, users may no longer increase their market exposure. Only actions
|
|
/// that reduce their position are still allowed.
|
|
pub reduce_only: u8,
|
|
pub force_close: u8,
|
|
|
|
pub padding4: [u8; 6],
|
|
|
|
/// Weights for full perp market health, if positive
|
|
pub maint_overall_asset_weight: I80F48,
|
|
pub init_overall_asset_weight: I80F48,
|
|
|
|
pub positive_pnl_liquidation_fee: I80F48,
|
|
|
|
pub reserved: [u8; 1888],
|
|
}
|
|
|
|
const_assert_eq!(
|
|
size_of::<PerpMarket>(),
|
|
32 + 2
|
|
+ 2
|
|
+ 1
|
|
+ 1
|
|
+ 16
|
|
+ 32
|
|
+ 32
|
|
+ 32
|
|
+ 32
|
|
+ 96
|
|
+ 288
|
|
+ 8
|
|
+ 8
|
|
+ 16 * 4
|
|
+ 8
|
|
+ 8
|
|
+ 1
|
|
+ 1
|
|
+ 8
|
|
+ 16 * 2
|
|
+ 8
|
|
+ 16 * 2
|
|
+ 8
|
|
+ 16 * 5
|
|
+ 4
|
|
+ 4 * 3
|
|
+ 8
|
|
+ 8
|
|
+ 1
|
|
+ 7
|
|
+ 3 * 16
|
|
+ 1888
|
|
);
|
|
const_assert_eq!(size_of::<PerpMarket>(), 2808);
|
|
const_assert_eq!(size_of::<PerpMarket>() % 8, 0);
|
|
|
|
impl PerpMarket {
|
|
pub fn name(&self) -> &str {
|
|
std::str::from_utf8(&self.name)
|
|
.unwrap()
|
|
.trim_matches(char::from(0))
|
|
}
|
|
|
|
pub fn is_reduce_only(&self) -> bool {
|
|
self.reduce_only == 1
|
|
}
|
|
|
|
pub fn is_force_close(&self) -> bool {
|
|
self.force_close == 1
|
|
}
|
|
|
|
pub fn elligible_for_group_insurance_fund(&self) -> bool {
|
|
self.group_insurance_fund == 1
|
|
}
|
|
|
|
pub fn set_elligible_for_group_insurance_fund(&mut self, v: bool) {
|
|
self.group_insurance_fund = u8::from(v);
|
|
}
|
|
|
|
pub fn settle_pnl_limit_factor(&self) -> I80F48 {
|
|
I80F48::from_num(self.settle_pnl_limit_factor)
|
|
}
|
|
|
|
pub fn gen_order_id(&mut self, side: Side, price_data: u64) -> u128 {
|
|
self.seq_num += 1;
|
|
orderbook::new_node_key(side, price_data, self.seq_num)
|
|
}
|
|
|
|
pub fn oracle_price(
|
|
&self,
|
|
oracle_acc: &impl KeyedAccountReader,
|
|
staleness_slot: Option<u64>,
|
|
) -> Result<I80F48> {
|
|
require_keys_eq!(self.oracle, *oracle_acc.key());
|
|
let (price, _) = oracle::oracle_price_and_state(
|
|
oracle_acc,
|
|
&self.oracle_config,
|
|
self.base_decimals,
|
|
staleness_slot,
|
|
)?;
|
|
|
|
Ok(price)
|
|
}
|
|
|
|
pub fn oracle_price_and_state(
|
|
&self,
|
|
oracle_acc: &impl KeyedAccountReader,
|
|
staleness_slot: Option<u64>,
|
|
) -> Result<(I80F48, OracleState)> {
|
|
require_keys_eq!(self.oracle, *oracle_acc.key());
|
|
oracle::oracle_price_and_state(
|
|
oracle_acc,
|
|
&self.oracle_config,
|
|
self.base_decimals,
|
|
staleness_slot,
|
|
)
|
|
}
|
|
|
|
pub fn stable_price(&self) -> I80F48 {
|
|
I80F48::from_num(self.stable_price_model.stable_price)
|
|
}
|
|
|
|
/// Use current order book price and index price to update the instantaneous funding
|
|
pub fn update_funding_and_stable_price(
|
|
&mut self,
|
|
book: &Orderbook,
|
|
oracle_price: I80F48,
|
|
oracle_state: OracleState,
|
|
now_ts: u64,
|
|
) -> Result<()> {
|
|
if now_ts <= self.funding_last_updated {
|
|
return Ok(());
|
|
}
|
|
|
|
let index_price = oracle_price;
|
|
let oracle_price_lots = self.native_price_to_lot(oracle_price);
|
|
|
|
// Get current book price & compare it to index price
|
|
let bid =
|
|
book.bookside(Side::Bid)
|
|
.impact_price(self.impact_quantity, now_ts, oracle_price_lots);
|
|
let ask =
|
|
book.bookside(Side::Ask)
|
|
.impact_price(self.impact_quantity, now_ts, oracle_price_lots);
|
|
|
|
let funding_rate = match (bid, ask) {
|
|
(Some(bid), Some(ask)) => {
|
|
// calculate mid-market rate
|
|
let mid_price = (bid + ask) / 2;
|
|
let book_price = self.lot_to_native_price(mid_price);
|
|
let diff = book_price / index_price - I80F48::ONE;
|
|
diff.clamp(self.min_funding, self.max_funding)
|
|
}
|
|
(Some(_bid), None) => self.max_funding,
|
|
(None, Some(_ask)) => self.min_funding,
|
|
(None, None) => I80F48::ZERO,
|
|
};
|
|
|
|
// Limit the maximal time interval that funding is applied for. This means we won't use
|
|
// the funding rate computed from a single orderbook snapshot for a very long time period
|
|
// in exceptional circumstances, like a solana downtime or the security council disabling
|
|
// funding updates.
|
|
let max_funding_timestep = 3600; // one hour
|
|
let diff_ts =
|
|
I80F48::from_num((now_ts - self.funding_last_updated as u64).min(max_funding_timestep));
|
|
|
|
let time_factor = diff_ts / DAY_I80F48;
|
|
let base_lot_size = I80F48::from_num(self.base_lot_size);
|
|
|
|
// The number of native quote that one base lot should pay in funding
|
|
let funding_delta = index_price * base_lot_size * funding_rate * time_factor;
|
|
|
|
self.long_funding += funding_delta;
|
|
self.short_funding += funding_delta;
|
|
self.funding_last_updated = now_ts;
|
|
|
|
self.stable_price_model
|
|
.update(now_ts, oracle_price.to_num());
|
|
|
|
emit!(PerpUpdateFundingLogV2 {
|
|
mango_group: self.group,
|
|
market_index: self.perp_market_index,
|
|
long_funding: self.long_funding.to_bits(),
|
|
short_funding: self.short_funding.to_bits(),
|
|
price: oracle_price.to_bits(),
|
|
oracle_slot: oracle_state.last_update_slot,
|
|
oracle_confidence: oracle_state.confidence.to_bits(),
|
|
oracle_type: oracle_state.oracle_type,
|
|
stable_price: self.stable_price().to_bits(),
|
|
fees_accrued: self.fees_accrued.to_bits(),
|
|
fees_settled: self.fees_settled.to_bits(),
|
|
open_interest: self.open_interest,
|
|
instantaneous_funding_rate: funding_rate.to_bits(),
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Convert from the price stored on the book to the price used in value calculations
|
|
pub fn lot_to_native_price(&self, price: i64) -> I80F48 {
|
|
I80F48::from_num(price) * I80F48::from_num(self.quote_lot_size)
|
|
/ I80F48::from_num(self.base_lot_size)
|
|
}
|
|
|
|
pub fn native_price_to_lot(&self, price: I80F48) -> i64 {
|
|
(price * I80F48::from_num(self.base_lot_size) / I80F48::from_num(self.quote_lot_size))
|
|
.to_num()
|
|
}
|
|
|
|
/// Is `native_price` an acceptable order for the `side` of this market, given `oracle_price`?
|
|
pub fn inside_price_limit(
|
|
&self,
|
|
side: Side,
|
|
native_price: I80F48,
|
|
oracle_price: I80F48,
|
|
) -> bool {
|
|
match side {
|
|
Side::Bid => native_price <= (self.maint_base_liab_weight * oracle_price),
|
|
Side::Ask => native_price >= (self.maint_base_asset_weight * oracle_price),
|
|
}
|
|
}
|
|
|
|
/// Socialize the loss in this account across all longs and shorts
|
|
pub fn socialize_loss(&mut self, loss: I80F48) -> Result<I80F48> {
|
|
require_gte!(0, loss);
|
|
|
|
// TODO convert into only socializing on one side
|
|
// native USDC per contract open interest
|
|
let socialized_loss = if self.open_interest == 0 {
|
|
// AUDIT: think about the following:
|
|
// This is kind of an unfortunate situation. This means socialized loss occurs on the
|
|
// last person to call settle_pnl on their profits. Any advice on better mechanism
|
|
// would be appreciated. Luckily, this will be an extremely rare situation.
|
|
I80F48::ZERO
|
|
} else {
|
|
loss / I80F48::from(self.open_interest)
|
|
};
|
|
self.long_funding -= socialized_loss;
|
|
self.short_funding += socialized_loss;
|
|
Ok(socialized_loss)
|
|
}
|
|
|
|
/// Returns the fee for settling `settlement` when the negative-pnl side has the given
|
|
/// health values.
|
|
pub fn compute_settle_fee(
|
|
&self,
|
|
settlement: I80F48,
|
|
source_liq_end_health: I80F48,
|
|
source_maint_health: I80F48,
|
|
) -> Result<I80F48> {
|
|
assert!(source_maint_health >= source_liq_end_health);
|
|
|
|
// A percentage fee is paid to the settler when the source account's health is low.
|
|
// That's because the settlement could avoid it getting liquidated: settling will
|
|
// increase its health by actualizing positive perp pnl.
|
|
let low_health_fee = if source_liq_end_health < 0 {
|
|
let fee_fraction = I80F48::from_num(self.settle_fee_fraction_low_health);
|
|
if source_maint_health < 0 {
|
|
settlement * fee_fraction
|
|
} else {
|
|
settlement
|
|
* fee_fraction
|
|
* (-source_liq_end_health / (source_maint_health - source_liq_end_health))
|
|
}
|
|
} else {
|
|
I80F48::ZERO
|
|
};
|
|
|
|
// The settler receives a flat fee
|
|
let flat_fee = if settlement >= self.settle_fee_amount_threshold {
|
|
I80F48::from_num(self.settle_fee_flat)
|
|
} else {
|
|
I80F48::ZERO
|
|
};
|
|
|
|
// Fees only apply when the settlement is large enough
|
|
let fee = (low_health_fee + flat_fee).min(settlement);
|
|
|
|
// Safety check to prevent any accidental negative transfer
|
|
require!(fee >= 0, MangoError::SettlementAmountMustBePositive);
|
|
|
|
Ok(fee)
|
|
}
|
|
|
|
/// Creates default market for tests
|
|
pub fn default_for_tests() -> PerpMarket {
|
|
PerpMarket {
|
|
group: Pubkey::new_unique(),
|
|
settle_token_index: 0,
|
|
perp_market_index: 0,
|
|
blocked1: 0,
|
|
group_insurance_fund: 0,
|
|
bump: 0,
|
|
base_decimals: 0,
|
|
name: Default::default(),
|
|
bids: Pubkey::new_unique(),
|
|
asks: Pubkey::new_unique(),
|
|
event_queue: Pubkey::new_unique(),
|
|
oracle: Pubkey::new_unique(),
|
|
oracle_config: OracleConfig {
|
|
conf_filter: I80F48::ZERO,
|
|
max_staleness_slots: -1,
|
|
reserved: [0; 72],
|
|
},
|
|
stable_price_model: StablePriceModel::default(),
|
|
quote_lot_size: 1,
|
|
base_lot_size: 1,
|
|
maint_base_asset_weight: I80F48::from(1),
|
|
init_base_asset_weight: I80F48::from(1),
|
|
maint_base_liab_weight: I80F48::from(1),
|
|
init_base_liab_weight: I80F48::from(1),
|
|
open_interest: 0,
|
|
seq_num: 0,
|
|
registration_time: 0,
|
|
min_funding: I80F48::ZERO,
|
|
max_funding: I80F48::ZERO,
|
|
impact_quantity: 0,
|
|
long_funding: I80F48::ZERO,
|
|
short_funding: I80F48::ZERO,
|
|
funding_last_updated: 0,
|
|
base_liquidation_fee: I80F48::ZERO,
|
|
maker_fee: I80F48::ZERO,
|
|
taker_fee: I80F48::ZERO,
|
|
fees_accrued: I80F48::ZERO,
|
|
fees_settled: I80F48::ZERO,
|
|
fee_penalty: 0.0,
|
|
settle_fee_flat: 0.0,
|
|
settle_fee_amount_threshold: 0.0,
|
|
settle_fee_fraction_low_health: 0.0,
|
|
settle_pnl_limit_factor: 0.2,
|
|
padding3: Default::default(),
|
|
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
|
|
reduce_only: 0,
|
|
force_close: 0,
|
|
padding4: Default::default(),
|
|
maint_overall_asset_weight: I80F48::ONE,
|
|
init_overall_asset_weight: I80F48::ONE,
|
|
positive_pnl_liquidation_fee: I80F48::ZERO,
|
|
reserved: [0; 1888],
|
|
}
|
|
}
|
|
}
|