361 lines
11 KiB
Rust
361 lines
11 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::logs::PerpUpdateFundingLog;
|
|
use crate::state::orderbook::Side;
|
|
use crate::state::{oracle, TokenIndex};
|
|
use crate::util::checked_math as cm;
|
|
|
|
use super::{orderbook, OracleConfig, Orderbook, StablePriceModel, DAY_I80F48};
|
|
|
|
pub type PerpMarketIndex = u16;
|
|
|
|
#[account(zero_copy(safe_bytemuck_derives))]
|
|
#[derive(Debug)]
|
|
pub struct PerpMarket {
|
|
// ABI: Clients rely on this being at offset 8
|
|
pub group: Pubkey,
|
|
|
|
pub settle_token_index: TokenIndex,
|
|
|
|
/// Lookup indices
|
|
pub perp_market_index: PerpMarketIndex,
|
|
|
|
/// May this market contribute positive values to health?
|
|
pub trusted_market: u8,
|
|
|
|
/// Is this market covered by the group insurance fund?
|
|
pub group_insurance_fund: u8,
|
|
|
|
/// PDA bump
|
|
pub bump: u8,
|
|
|
|
pub base_decimals: u8,
|
|
|
|
pub name: [u8; 16],
|
|
|
|
pub bids: Pubkey,
|
|
pub asks: Pubkey,
|
|
pub event_queue: Pubkey,
|
|
|
|
pub oracle: Pubkey,
|
|
pub oracle_config: OracleConfig,
|
|
pub stable_price_model: StablePriceModel,
|
|
|
|
/// Number of quote native that reresents min tick
|
|
pub quote_lot_size: i64,
|
|
/// Represents number of base native quantity
|
|
/// e.g. if base decimals for underlying asset are 6, base lot size is 100, and base position is 10000, then
|
|
/// UI position is 1
|
|
pub base_lot_size: i64,
|
|
|
|
// These weights apply to the base asset, the quote token is always assumed to be
|
|
// the health-reference token and have 1 for price and weights
|
|
pub maint_asset_weight: I80F48,
|
|
pub init_asset_weight: I80F48,
|
|
pub maint_liab_weight: I80F48,
|
|
pub init_liab_weight: I80F48,
|
|
|
|
pub open_interest: i64,
|
|
|
|
/// Total number of orders seen
|
|
pub seq_num: u64,
|
|
|
|
pub registration_time: u64,
|
|
|
|
/// Funding
|
|
pub min_funding: I80F48,
|
|
pub max_funding: I80F48,
|
|
pub impact_quantity: i64,
|
|
pub long_funding: I80F48,
|
|
pub short_funding: I80F48,
|
|
/// timestamp that funding was last updated in
|
|
pub funding_last_updated: u64,
|
|
|
|
/// Fees
|
|
pub liquidation_fee: I80F48,
|
|
pub maker_fee: I80F48,
|
|
pub taker_fee: I80F48,
|
|
/// Fees accrued in native quote currency
|
|
pub fees_accrued: I80F48,
|
|
/// Fees settled in native quote currency
|
|
pub fees_settled: I80F48,
|
|
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 fees.
|
|
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
|
|
/// Fraction of perp base value (i.e. base_lots * entry_price_in_lots) of unrealized
|
|
/// positive pnl that can be settled each window.
|
|
/// Set to a negative value to disable the limit.
|
|
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,
|
|
|
|
pub reduce_only: u8,
|
|
|
|
pub reserved: [u8; 1943],
|
|
}
|
|
|
|
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
|
|
+ 1943
|
|
);
|
|
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 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 = if v { 1 } else { 0 };
|
|
}
|
|
|
|
pub fn trusted_market(&self) -> bool {
|
|
self.trusted_market == 1
|
|
}
|
|
|
|
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());
|
|
oracle::oracle_price(
|
|
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,
|
|
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.impact_price(Side::Bid, self.impact_quantity, now_ts, oracle_price_lots);
|
|
let ask = book.impact_price(Side::Ask, self.impact_quantity, now_ts, oracle_price_lots);
|
|
|
|
let diff_price = match (bid, ask) {
|
|
(Some(bid), Some(ask)) => {
|
|
// calculate mid-market rate
|
|
let mid_price = bid.checked_add(ask).unwrap() / 2;
|
|
let book_price = self.lot_to_native_price(mid_price);
|
|
let diff = cm!(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,
|
|
};
|
|
|
|
let diff_ts = I80F48::from_num(now_ts - self.funding_last_updated as u64);
|
|
let time_factor = cm!(diff_ts / DAY_I80F48);
|
|
let base_lot_size = I80F48::from_num(self.base_lot_size);
|
|
let funding_delta = cm!(index_price * diff_price * base_lot_size * 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!(PerpUpdateFundingLog {
|
|
mango_group: self.group,
|
|
market_index: self.perp_market_index,
|
|
long_funding: self.long_funding.to_bits(),
|
|
short_funding: self.long_funding.to_bits(),
|
|
price: oracle_price.to_bits(),
|
|
stable_price: self.stable_price().to_bits(),
|
|
fees_accrued: self.fees_accrued.to_bits(),
|
|
open_interest: self.open_interest,
|
|
instantaneous_funding_rate: diff_price.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)
|
|
.checked_mul(I80F48::from_num(self.quote_lot_size))
|
|
.unwrap()
|
|
.checked_div(I80F48::from_num(self.base_lot_size))
|
|
.unwrap()
|
|
}
|
|
|
|
pub fn native_price_to_lot(&self, price: I80F48) -> i64 {
|
|
price
|
|
.checked_mul(I80F48::from_num(self.base_lot_size))
|
|
.unwrap()
|
|
.checked_div(I80F48::from_num(self.quote_lot_size))
|
|
.unwrap()
|
|
.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 <= cm!(self.maint_liab_weight * oracle_price),
|
|
Side::Ask => native_price >= cm!(self.maint_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 {
|
|
cm!(loss / I80F48::from(self.open_interest))
|
|
};
|
|
self.long_funding -= socialized_loss;
|
|
self.short_funding += socialized_loss;
|
|
Ok(socialized_loss)
|
|
}
|
|
|
|
/// Creates default market for tests
|
|
pub fn default_for_tests() -> PerpMarket {
|
|
PerpMarket {
|
|
group: Pubkey::new_unique(),
|
|
settle_token_index: 0,
|
|
perp_market_index: 0,
|
|
trusted_market: 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_asset_weight: I80F48::from(1),
|
|
init_asset_weight: I80F48::from(1),
|
|
maint_liab_weight: I80F48::from(1),
|
|
init_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,
|
|
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,
|
|
reserved: [0; 1943],
|
|
}
|
|
}
|
|
}
|