Added perp_settle_pnl instruction
This commit is contained in:
parent
511db72f8e
commit
97eed4081d
|
@ -39,6 +39,16 @@ pub enum MangoError {
|
||||||
InsufficentBankVaultFunds,
|
InsufficentBankVaultFunds,
|
||||||
#[msg("account is currently being liquidated")]
|
#[msg("account is currently being liquidated")]
|
||||||
BeingLiquidated,
|
BeingLiquidated,
|
||||||
|
#[msg("invalid bank")]
|
||||||
|
InvalidBank,
|
||||||
|
#[msg("account profitability is mismatched")]
|
||||||
|
ProfitabilityMismatch,
|
||||||
|
#[msg("cannot settle with self")]
|
||||||
|
CannotSettleWithSelf,
|
||||||
|
#[msg("perp position does not exist")]
|
||||||
|
PerpPositionDoesNotExist,
|
||||||
|
#[msg("max settle amount must be greater than zero")]
|
||||||
|
MaxSettleAmountMustBeGreaterThanZero,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Contextable {
|
pub trait Contextable {
|
||||||
|
|
|
@ -20,6 +20,7 @@ pub use perp_consume_events::*;
|
||||||
pub use perp_create_market::*;
|
pub use perp_create_market::*;
|
||||||
pub use perp_edit_market::*;
|
pub use perp_edit_market::*;
|
||||||
pub use perp_place_order::*;
|
pub use perp_place_order::*;
|
||||||
|
pub use perp_settle_pnl::*;
|
||||||
pub use perp_update_funding::*;
|
pub use perp_update_funding::*;
|
||||||
pub use serum3_cancel_all_orders::*;
|
pub use serum3_cancel_all_orders::*;
|
||||||
pub use serum3_cancel_order::*;
|
pub use serum3_cancel_order::*;
|
||||||
|
@ -64,6 +65,7 @@ mod perp_consume_events;
|
||||||
mod perp_create_market;
|
mod perp_create_market;
|
||||||
mod perp_edit_market;
|
mod perp_edit_market;
|
||||||
mod perp_place_order;
|
mod perp_place_order;
|
||||||
|
mod perp_settle_pnl;
|
||||||
mod perp_update_funding;
|
mod perp_update_funding;
|
||||||
mod serum3_cancel_all_orders;
|
mod serum3_cancel_all_orders;
|
||||||
mod serum3_cancel_order;
|
mod serum3_cancel_order;
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
use checked_math as cm;
|
||||||
|
use fixed::types::I80F48;
|
||||||
|
|
||||||
|
use crate::accounts_zerocopy::*;
|
||||||
|
use crate::error::*;
|
||||||
|
use crate::state::compute_health;
|
||||||
|
use crate::state::new_fixed_order_account_retriever;
|
||||||
|
use crate::state::Bank;
|
||||||
|
use crate::state::HealthType;
|
||||||
|
use crate::state::MangoAccount;
|
||||||
|
use crate::state::TokenPosition;
|
||||||
|
use crate::state::QUOTE_TOKEN_INDEX;
|
||||||
|
use crate::state::{oracle_price, AccountLoaderDynamic, Group, PerpMarket};
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct PerpSettlePnl<'info> {
|
||||||
|
pub group: AccountLoader<'info, Group>,
|
||||||
|
|
||||||
|
#[account(has_one = group, has_one = oracle)]
|
||||||
|
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||||
|
|
||||||
|
// This account MUST be profitable
|
||||||
|
#[account(mut, has_one = group)]
|
||||||
|
pub account_a: AccountLoaderDynamic<'info, MangoAccount>,
|
||||||
|
// This account MUST have a loss
|
||||||
|
#[account(mut, has_one = group)]
|
||||||
|
pub account_b: AccountLoaderDynamic<'info, MangoAccount>,
|
||||||
|
|
||||||
|
pub oracle: UncheckedAccount<'info>,
|
||||||
|
|
||||||
|
#[account(mut, has_one = group)]
|
||||||
|
pub quote_bank: AccountLoader<'info, Bank>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>, max_settle_amount: I80F48) -> Result<()> {
|
||||||
|
// Cannot settle with yourself
|
||||||
|
require!(
|
||||||
|
ctx.accounts.account_a.to_account_info().key
|
||||||
|
!= ctx.accounts.account_b.to_account_info().key,
|
||||||
|
MangoError::CannotSettleWithSelf
|
||||||
|
);
|
||||||
|
|
||||||
|
// max_settle_amount must greater than zero
|
||||||
|
require!(
|
||||||
|
max_settle_amount > 0,
|
||||||
|
MangoError::MaxSettleAmountMustBeGreaterThanZero
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut account_a = ctx.accounts.account_a.load_mut()?;
|
||||||
|
let mut account_b = ctx.accounts.account_b.load_mut()?;
|
||||||
|
let mut bank = ctx.accounts.quote_bank.load_mut()?;
|
||||||
|
let perp_market = ctx.accounts.perp_market.load()?;
|
||||||
|
|
||||||
|
// Verify that the bank is the quote currency bank
|
||||||
|
require!(
|
||||||
|
bank.token_index == QUOTE_TOKEN_INDEX,
|
||||||
|
MangoError::InvalidBank
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get oracle price for market. Price is validated inside
|
||||||
|
let oracle_price = oracle_price(
|
||||||
|
&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?,
|
||||||
|
perp_market.oracle_config.conf_filter,
|
||||||
|
perp_market.base_token_decimals,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Fetch perp positions for accounts
|
||||||
|
let mut a_perp_position = account_a.perp_position_mut(perp_market.perp_market_index)?;
|
||||||
|
let mut b_perp_position = account_b.perp_position_mut(perp_market.perp_market_index)?;
|
||||||
|
|
||||||
|
// Settle funding before settling any PnL
|
||||||
|
a_perp_position.settle_funding(&perp_market);
|
||||||
|
b_perp_position.settle_funding(&perp_market);
|
||||||
|
|
||||||
|
// Calculate PnL for each account
|
||||||
|
let a_base_native = a_perp_position.base_position_native(&perp_market);
|
||||||
|
let b_base_native = b_perp_position.base_position_native(&perp_market);
|
||||||
|
let a_pnl: I80F48 = cm!(a_perp_position.quote_position_native + a_base_native * oracle_price);
|
||||||
|
let b_pnl: I80F48 = cm!(b_perp_position.quote_position_native + b_base_native * oracle_price);
|
||||||
|
|
||||||
|
// Account A must be profitable, and B must be unprofitable
|
||||||
|
// PnL must be opposite signs for there to be a settlement
|
||||||
|
require!(a_pnl.is_positive(), MangoError::ProfitabilityMismatch);
|
||||||
|
require!(b_pnl.is_negative(), MangoError::ProfitabilityMismatch);
|
||||||
|
|
||||||
|
// Settle for the maximum possible capped to max_settle_amount
|
||||||
|
let settlement = a_pnl.abs().min(b_pnl.abs()).min(max_settle_amount);
|
||||||
|
a_perp_position.quote_position_native = cm!(a_perp_position.quote_position_native - settlement);
|
||||||
|
b_perp_position.quote_position_native = cm!(b_perp_position.quote_position_native + settlement);
|
||||||
|
|
||||||
|
// Update the account's net_settled with the new PnL
|
||||||
|
let settlement_i64 = settlement.checked_to_num::<i64>().unwrap();
|
||||||
|
account_a.fixed.net_settled = cm!(account_a.fixed.net_settled + settlement_i64);
|
||||||
|
account_b.fixed.net_settled = cm!(account_b.fixed.net_settled - settlement_i64);
|
||||||
|
|
||||||
|
// Transfer token balances
|
||||||
|
// TODO: Need to guarantee that QUOTE_TOKEN_INDEX token exists at this point. I.E. create it when placing perp order.
|
||||||
|
let a_token_position = account_a.ensure_token_position(QUOTE_TOKEN_INDEX)?.0;
|
||||||
|
let b_token_position = account_b.ensure_token_position(QUOTE_TOKEN_INDEX)?.0;
|
||||||
|
transfer_token_internal(&mut bank, b_token_position, a_token_position, settlement)?;
|
||||||
|
|
||||||
|
// Bank is dropped to prevent re-borrow from remaining_accounts
|
||||||
|
drop(bank);
|
||||||
|
|
||||||
|
// Verify that the result of settling did not violate the health of the account that lost money
|
||||||
|
let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account_b.borrow())?;
|
||||||
|
let health = compute_health(&account_b.borrow(), HealthType::Init, &retriever)?;
|
||||||
|
require!(health >= 0, MangoError::HealthMustBePositive);
|
||||||
|
|
||||||
|
msg!("settled pnl = {}", settlement);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transfer_token_internal(
|
||||||
|
bank: &mut Bank,
|
||||||
|
from_position: &mut TokenPosition,
|
||||||
|
to_position: &mut TokenPosition,
|
||||||
|
native_amount: I80F48,
|
||||||
|
) -> Result<()> {
|
||||||
|
bank.deposit(to_position, native_amount)?;
|
||||||
|
bank.withdraw_with_fee(from_position, native_amount)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -497,6 +497,9 @@ pub mod mango_v4 {
|
||||||
instructions::perp_update_funding(ctx)
|
instructions::perp_update_funding(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>, max_settle_amount: I80F48) -> Result<()> {
|
||||||
|
instructions::perp_settle_pnl(ctx, max_settle_amount)
|
||||||
|
}
|
||||||
// TODO
|
// TODO
|
||||||
|
|
||||||
// perp_force_cancel_order
|
// perp_force_cancel_order
|
||||||
|
@ -504,7 +507,7 @@ pub mod mango_v4 {
|
||||||
// liquidate_token_and_perp
|
// liquidate_token_and_perp
|
||||||
// liquidate_perp_and_perp
|
// liquidate_perp_and_perp
|
||||||
|
|
||||||
// settle_* - settle_funds, settle_pnl, settle_fees
|
// settle_* - settle_funds, settle_fees
|
||||||
|
|
||||||
// resolve_banktruptcy
|
// resolve_banktruptcy
|
||||||
|
|
||||||
|
|
|
@ -690,6 +690,18 @@ impl<
|
||||||
get_helper_mut(self.dynamic_mut(), offset)
|
get_helper_mut(self.dynamic_mut(), offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn perp_position_mut(
|
||||||
|
&mut self,
|
||||||
|
market_index: PerpMarketIndex,
|
||||||
|
) -> Result<&mut PerpPosition> {
|
||||||
|
let raw_index_opt = self
|
||||||
|
.all_perp_positions()
|
||||||
|
.position(|p| p.is_active_for_market(market_index));
|
||||||
|
raw_index_opt
|
||||||
|
.map(|raw_index| self.perp_position_mut_by_raw_index(raw_index))
|
||||||
|
.ok_or_else(|| error!(MangoError::PerpPositionDoesNotExist))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn ensure_perp_position(
|
pub fn ensure_perp_position(
|
||||||
&mut self,
|
&mut self,
|
||||||
perp_market_index: PerpMarketIndex,
|
perp_market_index: PerpMarketIndex,
|
||||||
|
@ -1205,6 +1217,17 @@ mod tests {
|
||||||
assert_eq!(pos.market_index, 42);
|
assert_eq!(pos.market_index, 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let pos_res = account.perp_position_mut(1);
|
||||||
|
assert!(pos_res.is_ok());
|
||||||
|
assert_eq!(pos_res.unwrap().market_index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let pos_res = account.perp_position_mut(99);
|
||||||
|
assert!(pos_res.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
account.deactivate_perp_position(1);
|
account.deactivate_perp_position(1);
|
||||||
|
|
||||||
|
@ -1228,16 +1251,5 @@ mod tests {
|
||||||
assert!(account.perp_position(8).is_ok());
|
assert!(account.perp_position(8).is_ok());
|
||||||
assert!(account.perp_position(42).is_ok());
|
assert!(account.perp_position(42).is_ok());
|
||||||
assert_eq!(account.active_perp_positions().count(), 2);
|
assert_eq!(account.active_perp_positions().count(), 2);
|
||||||
|
|
||||||
/*{
|
|
||||||
let (pos, raw) = account.perp_position_mut(42).unwrap();
|
|
||||||
assert_eq!(pos.perp_index, 42);
|
|
||||||
assert_eq!(raw, 2);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let (pos, raw) = account.perp_position_mut(8).unwrap();
|
|
||||||
assert_eq!(pos.perp_index, 8);
|
|
||||||
assert_eq!(raw, 1);
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -235,6 +235,11 @@ impl PerpPosition {
|
||||||
self.market_index == market_index
|
self.market_index == market_index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return base position in native units for a perp market
|
||||||
|
pub fn base_position_native(&self, market: &PerpMarket) -> I80F48 {
|
||||||
|
I80F48::from(cm!(self.base_position_lots * market.base_lot_size))
|
||||||
|
}
|
||||||
|
|
||||||
/// This assumes settle_funding was already called
|
/// This assumes settle_funding was already called
|
||||||
pub fn change_base_position(&mut self, perp_market: &mut PerpMarket, base_change: i64) {
|
pub fn change_base_position(&mut self, perp_market: &mut PerpMarket, base_change: i64) {
|
||||||
let start = self.base_position_lots;
|
let start = self.base_position_lots;
|
||||||
|
|
|
@ -2481,6 +2481,61 @@ impl ClientInstruction for PerpUpdateFundingInstruction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PerpSettlePnlInstruction {
|
||||||
|
pub group: Pubkey,
|
||||||
|
pub account_a: Pubkey,
|
||||||
|
pub account_b: Pubkey,
|
||||||
|
pub perp_market: Pubkey,
|
||||||
|
pub oracle: Pubkey,
|
||||||
|
pub quote_bank: Pubkey,
|
||||||
|
pub max_settle_amount: I80F48,
|
||||||
|
}
|
||||||
|
#[async_trait::async_trait(?Send)]
|
||||||
|
impl ClientInstruction for PerpSettlePnlInstruction {
|
||||||
|
type Accounts = mango_v4::accounts::PerpSettlePnl;
|
||||||
|
type Instruction = mango_v4::instruction::PerpSettlePnl;
|
||||||
|
async fn to_instruction(
|
||||||
|
&self,
|
||||||
|
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||||
|
) -> (Self::Accounts, instruction::Instruction) {
|
||||||
|
let program_id = mango_v4::id();
|
||||||
|
let instruction = Self::Instruction {
|
||||||
|
max_settle_amount: self.max_settle_amount,
|
||||||
|
};
|
||||||
|
let accounts = Self::Accounts {
|
||||||
|
group: self.group,
|
||||||
|
perp_market: self.perp_market,
|
||||||
|
account_a: self.account_a,
|
||||||
|
account_b: self.account_b,
|
||||||
|
oracle: self.oracle,
|
||||||
|
quote_bank: self.quote_bank,
|
||||||
|
};
|
||||||
|
|
||||||
|
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
|
||||||
|
let account_b = account_loader
|
||||||
|
.load_mango_account(&self.account_b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let health_check_metas = derive_health_check_remaining_account_metas(
|
||||||
|
&account_loader,
|
||||||
|
&account_b,
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
Some(perp_market.perp_market_index),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut instruction = make_instruction(program_id, &accounts, instruction);
|
||||||
|
instruction.accounts.extend(health_check_metas);
|
||||||
|
|
||||||
|
(accounts, instruction)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signers(&self) -> Vec<&Keypair> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct BenchmarkInstruction {}
|
pub struct BenchmarkInstruction {}
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl ClientInstruction for BenchmarkInstruction {
|
impl ClientInstruction for BenchmarkInstruction {
|
||||||
|
|
|
@ -0,0 +1,783 @@
|
||||||
|
#![cfg(all(feature = "test-bpf"))]
|
||||||
|
|
||||||
|
use anchor_lang::prelude::ErrorCode;
|
||||||
|
use fixed::types::I80F48;
|
||||||
|
use mango_v4::{error::MangoError, state::*};
|
||||||
|
use program_test::*;
|
||||||
|
use solana_program::instruction::InstructionError;
|
||||||
|
use solana_program_test::*;
|
||||||
|
use solana_sdk::{signature::Keypair, transaction::TransactionError, transport::TransportError};
|
||||||
|
|
||||||
|
mod program_test;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_perp_settle_pnl() -> Result<(), TransportError> {
|
||||||
|
let context = TestContext::new().await;
|
||||||
|
let solana = &context.solana.clone();
|
||||||
|
|
||||||
|
let admin = &Keypair::new();
|
||||||
|
let owner = &context.users[0].key;
|
||||||
|
let payer = &context.users[1].key;
|
||||||
|
let mints = &context.mints[0..2];
|
||||||
|
let payer_mint_accounts = &context.users[1].token_accounts[0..=2];
|
||||||
|
|
||||||
|
let initial_token_deposit = 10_000;
|
||||||
|
|
||||||
|
//
|
||||||
|
// SETUP: Create a group and an account
|
||||||
|
//
|
||||||
|
|
||||||
|
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
|
||||||
|
admin,
|
||||||
|
payer,
|
||||||
|
mints,
|
||||||
|
}
|
||||||
|
.create(solana)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let account_0 = send_tx(
|
||||||
|
solana,
|
||||||
|
AccountCreateInstruction {
|
||||||
|
account_num: 0,
|
||||||
|
token_count: 16,
|
||||||
|
serum3_count: 8,
|
||||||
|
perp_count: 8,
|
||||||
|
perp_oo_count: 8,
|
||||||
|
group,
|
||||||
|
owner,
|
||||||
|
payer,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.account;
|
||||||
|
|
||||||
|
let account_1 = send_tx(
|
||||||
|
solana,
|
||||||
|
AccountCreateInstruction {
|
||||||
|
account_num: 1,
|
||||||
|
token_count: 16,
|
||||||
|
serum3_count: 8,
|
||||||
|
perp_count: 8,
|
||||||
|
perp_oo_count: 8,
|
||||||
|
group,
|
||||||
|
owner,
|
||||||
|
payer,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.account;
|
||||||
|
|
||||||
|
//
|
||||||
|
// SETUP: Deposit user funds
|
||||||
|
//
|
||||||
|
{
|
||||||
|
let deposit_amount = initial_token_deposit;
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenDepositInstruction {
|
||||||
|
amount: deposit_amount,
|
||||||
|
account: account_0,
|
||||||
|
token_account: payer_mint_accounts[0],
|
||||||
|
token_authority: payer.clone(),
|
||||||
|
bank_index: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenDepositInstruction {
|
||||||
|
amount: deposit_amount,
|
||||||
|
account: account_0,
|
||||||
|
token_account: payer_mint_accounts[1],
|
||||||
|
token_authority: payer.clone(),
|
||||||
|
bank_index: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let deposit_amount = initial_token_deposit;
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenDepositInstruction {
|
||||||
|
amount: deposit_amount,
|
||||||
|
account: account_1,
|
||||||
|
token_account: payer_mint_accounts[0],
|
||||||
|
token_authority: payer.clone(),
|
||||||
|
bank_index: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
TokenDepositInstruction {
|
||||||
|
amount: deposit_amount,
|
||||||
|
account: account_1,
|
||||||
|
token_account: payer_mint_accounts[1],
|
||||||
|
token_authority: payer.clone(),
|
||||||
|
bank_index: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// TEST: Create a perp market
|
||||||
|
//
|
||||||
|
let mango_v4::accounts::PerpCreateMarket {
|
||||||
|
perp_market,
|
||||||
|
asks,
|
||||||
|
bids,
|
||||||
|
event_queue,
|
||||||
|
..
|
||||||
|
} = send_tx(
|
||||||
|
solana,
|
||||||
|
PerpCreateMarketInstruction {
|
||||||
|
group,
|
||||||
|
admin,
|
||||||
|
oracle: tokens[0].oracle,
|
||||||
|
asks: context
|
||||||
|
.solana
|
||||||
|
.create_account_for_type::<BookSide>(&mango_v4::id())
|
||||||
|
.await,
|
||||||
|
bids: context
|
||||||
|
.solana
|
||||||
|
.create_account_for_type::<BookSide>(&mango_v4::id())
|
||||||
|
.await,
|
||||||
|
event_queue: {
|
||||||
|
context
|
||||||
|
.solana
|
||||||
|
.create_account_for_type::<EventQueue>(&mango_v4::id())
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
payer,
|
||||||
|
perp_market_index: 0,
|
||||||
|
base_token_index: tokens[0].index,
|
||||||
|
base_token_decimals: tokens[0].mint.decimals,
|
||||||
|
quote_lot_size: 10,
|
||||||
|
base_lot_size: 100,
|
||||||
|
maint_asset_weight: 0.975,
|
||||||
|
init_asset_weight: 0.95,
|
||||||
|
maint_liab_weight: 1.025,
|
||||||
|
init_liab_weight: 1.05,
|
||||||
|
liquidation_fee: 0.012,
|
||||||
|
maker_fee: 0.0002,
|
||||||
|
taker_fee: 0.000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
//
|
||||||
|
// TEST: Create another perp market
|
||||||
|
//
|
||||||
|
let mango_v4::accounts::PerpCreateMarket {
|
||||||
|
perp_market: perp_market_2,
|
||||||
|
..
|
||||||
|
} = send_tx(
|
||||||
|
solana,
|
||||||
|
PerpCreateMarketInstruction {
|
||||||
|
group,
|
||||||
|
admin,
|
||||||
|
oracle: tokens[1].oracle,
|
||||||
|
asks: context
|
||||||
|
.solana
|
||||||
|
.create_account_for_type::<BookSide>(&mango_v4::id())
|
||||||
|
.await,
|
||||||
|
bids: context
|
||||||
|
.solana
|
||||||
|
.create_account_for_type::<BookSide>(&mango_v4::id())
|
||||||
|
.await,
|
||||||
|
event_queue: {
|
||||||
|
context
|
||||||
|
.solana
|
||||||
|
.create_account_for_type::<EventQueue>(&mango_v4::id())
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
payer,
|
||||||
|
perp_market_index: 1,
|
||||||
|
base_token_index: tokens[1].index,
|
||||||
|
base_token_decimals: tokens[1].mint.decimals,
|
||||||
|
quote_lot_size: 10,
|
||||||
|
base_lot_size: 100,
|
||||||
|
maint_asset_weight: 0.975,
|
||||||
|
init_asset_weight: 0.95,
|
||||||
|
maint_liab_weight: 1.025,
|
||||||
|
init_liab_weight: 1.05,
|
||||||
|
liquidation_fee: 0.012,
|
||||||
|
maker_fee: 0.0002,
|
||||||
|
taker_fee: 0.000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let price_lots = {
|
||||||
|
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||||
|
perp_market.native_price_to_lot(I80F48::from(1000))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the initial oracle price
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
StubOracleSetInstruction {
|
||||||
|
group,
|
||||||
|
admin,
|
||||||
|
mint: mints[0].pubkey,
|
||||||
|
payer,
|
||||||
|
price: "1000.0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Place orders and create a position
|
||||||
|
//
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpPlaceOrderInstruction {
|
||||||
|
group,
|
||||||
|
account: account_0,
|
||||||
|
perp_market,
|
||||||
|
asks,
|
||||||
|
bids,
|
||||||
|
event_queue,
|
||||||
|
oracle: tokens[0].oracle,
|
||||||
|
owner,
|
||||||
|
side: Side::Bid,
|
||||||
|
price_lots,
|
||||||
|
max_base_lots: 1,
|
||||||
|
max_quote_lots: i64::MAX,
|
||||||
|
client_order_id: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpPlaceOrderInstruction {
|
||||||
|
group,
|
||||||
|
account: account_1,
|
||||||
|
perp_market,
|
||||||
|
asks,
|
||||||
|
bids,
|
||||||
|
event_queue,
|
||||||
|
oracle: tokens[0].oracle,
|
||||||
|
owner,
|
||||||
|
side: Side::Ask,
|
||||||
|
price_lots,
|
||||||
|
max_base_lots: 1,
|
||||||
|
max_quote_lots: i64::MAX,
|
||||||
|
client_order_id: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpConsumeEventsInstruction {
|
||||||
|
group,
|
||||||
|
perp_market,
|
||||||
|
event_queue,
|
||||||
|
mango_accounts: vec![account_0, account_1],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
|
|
||||||
|
assert_eq!(mango_account_0.perps[0].base_position_lots, 1);
|
||||||
|
assert_eq!(mango_account_1.perps[0].base_position_lots, -1);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.perps[0].quote_position_native.round(),
|
||||||
|
-100_020
|
||||||
|
);
|
||||||
|
assert_eq!(mango_account_1.perps[0].quote_position_native, 100_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bank must be valid for quote currency
|
||||||
|
let result = send_tx(
|
||||||
|
solana,
|
||||||
|
PerpSettlePnlInstruction {
|
||||||
|
group,
|
||||||
|
account_a: account_1,
|
||||||
|
account_b: account_0,
|
||||||
|
perp_market,
|
||||||
|
oracle: tokens[0].oracle,
|
||||||
|
quote_bank: tokens[1].bank,
|
||||||
|
max_settle_amount: I80F48::MAX,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_mango_error(
|
||||||
|
&result,
|
||||||
|
MangoError::InvalidBank.into(),
|
||||||
|
"Bank must be valid for quote currency".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Oracle must be valid for the perp market
|
||||||
|
let result = send_tx(
|
||||||
|
solana,
|
||||||
|
PerpSettlePnlInstruction {
|
||||||
|
group,
|
||||||
|
account_a: account_1,
|
||||||
|
account_b: account_0,
|
||||||
|
perp_market,
|
||||||
|
oracle: tokens[1].oracle, // Using oracle for token 1 not 0
|
||||||
|
quote_bank: tokens[0].bank,
|
||||||
|
max_settle_amount: I80F48::MAX,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_mango_error(
|
||||||
|
&result,
|
||||||
|
ErrorCode::ConstraintHasOne.into(),
|
||||||
|
"Oracle must be valid for perp market".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cannot settle with yourself
|
||||||
|
let result = send_tx(
|
||||||
|
solana,
|
||||||
|
PerpSettlePnlInstruction {
|
||||||
|
group,
|
||||||
|
account_a: account_0,
|
||||||
|
account_b: account_0,
|
||||||
|
perp_market,
|
||||||
|
oracle: tokens[0].oracle,
|
||||||
|
quote_bank: tokens[0].bank,
|
||||||
|
max_settle_amount: I80F48::MAX,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_mango_error(
|
||||||
|
&result,
|
||||||
|
MangoError::CannotSettleWithSelf.into(),
|
||||||
|
"Cannot settle with yourself".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cannot settle position that does not exist
|
||||||
|
let result = send_tx(
|
||||||
|
solana,
|
||||||
|
PerpSettlePnlInstruction {
|
||||||
|
group,
|
||||||
|
account_a: account_0,
|
||||||
|
account_b: account_1,
|
||||||
|
perp_market: perp_market_2,
|
||||||
|
oracle: tokens[1].oracle,
|
||||||
|
quote_bank: tokens[0].bank,
|
||||||
|
max_settle_amount: I80F48::MAX,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_mango_error(
|
||||||
|
&result,
|
||||||
|
MangoError::PerpPositionDoesNotExist.into(),
|
||||||
|
"Cannot settle a position that does not exist".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// max_settle_amount must be greater than zero
|
||||||
|
for max_amnt in vec![I80F48::ZERO, I80F48::from(-100)] {
|
||||||
|
let result = send_tx(
|
||||||
|
solana,
|
||||||
|
PerpSettlePnlInstruction {
|
||||||
|
group,
|
||||||
|
account_a: account_0,
|
||||||
|
account_b: account_1,
|
||||||
|
perp_market: perp_market,
|
||||||
|
oracle: tokens[0].oracle,
|
||||||
|
quote_bank: tokens[0].bank,
|
||||||
|
max_settle_amount: max_amnt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_mango_error(
|
||||||
|
&result,
|
||||||
|
MangoError::MaxSettleAmountMustBeGreaterThanZero.into(),
|
||||||
|
"max_settle_amount must be greater than zero".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Test funding settlement
|
||||||
|
|
||||||
|
{
|
||||||
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
|
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.tokens[0].native(&bank).round(),
|
||||||
|
initial_token_deposit,
|
||||||
|
"account 0 has expected amount of tokens"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_1.tokens[0].native(&bank).round(),
|
||||||
|
initial_token_deposit,
|
||||||
|
"account 1 has expected amount of tokens"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try and settle with high price
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
StubOracleSetInstruction {
|
||||||
|
group,
|
||||||
|
admin,
|
||||||
|
mint: mints[0].pubkey,
|
||||||
|
payer,
|
||||||
|
price: "1200.0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Account a must be the profitable one
|
||||||
|
let result = send_tx(
|
||||||
|
solana,
|
||||||
|
PerpSettlePnlInstruction {
|
||||||
|
group,
|
||||||
|
account_a: account_1,
|
||||||
|
account_b: account_0,
|
||||||
|
perp_market,
|
||||||
|
oracle: tokens[0].oracle,
|
||||||
|
quote_bank: tokens[0].bank,
|
||||||
|
max_settle_amount: I80F48::MAX,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_mango_error(
|
||||||
|
&result,
|
||||||
|
MangoError::ProfitabilityMismatch.into(),
|
||||||
|
"Account a must be the profitable one".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = send_tx(
|
||||||
|
solana,
|
||||||
|
PerpSettlePnlInstruction {
|
||||||
|
group,
|
||||||
|
account_a: account_0,
|
||||||
|
account_b: account_1,
|
||||||
|
perp_market,
|
||||||
|
oracle: tokens[0].oracle,
|
||||||
|
quote_bank: tokens[0].bank,
|
||||||
|
max_settle_amount: I80F48::MAX,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_mango_error(
|
||||||
|
&result,
|
||||||
|
MangoError::HealthMustBePositive.into(),
|
||||||
|
"Health of losing account must be positive to settle".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Change the oracle to a more reasonable price
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
StubOracleSetInstruction {
|
||||||
|
group,
|
||||||
|
admin,
|
||||||
|
mint: mints[0].pubkey,
|
||||||
|
payer,
|
||||||
|
price: "1005.0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let expected_pnl_0 = I80F48::from(480); // Less due to fees
|
||||||
|
let expected_pnl_1 = I80F48::from(-500);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
|
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||||
|
assert_eq!(
|
||||||
|
get_pnl_native(&mango_account_0.perps[0], &perp_market, I80F48::from(1005)).round(),
|
||||||
|
expected_pnl_0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_pnl_native(&mango_account_1.perps[0], &perp_market, I80F48::from(1005)),
|
||||||
|
expected_pnl_1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
solana.advance_clock().await;
|
||||||
|
|
||||||
|
// Partially execute the settle
|
||||||
|
let partial_settle_amount = I80F48::from(200);
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpSettlePnlInstruction {
|
||||||
|
group,
|
||||||
|
account_a: account_0,
|
||||||
|
account_b: account_1,
|
||||||
|
perp_market,
|
||||||
|
oracle: tokens[0].oracle,
|
||||||
|
quote_bank: tokens[0].bank,
|
||||||
|
max_settle_amount: partial_settle_amount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||||
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.perps[0].base_position_lots, 1,
|
||||||
|
"base position unchanged for account 0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_1.perps[0].base_position_lots, -1,
|
||||||
|
"base position unchanged for account 1"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.perps[0].quote_position_native.round(),
|
||||||
|
I80F48::from(-100_020) - partial_settle_amount,
|
||||||
|
"quote position reduced for profitable position by max_settle_amount"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_1.perps[0].quote_position_native.round(),
|
||||||
|
I80F48::from(100_000) + partial_settle_amount,
|
||||||
|
"quote position increased for losing position by opposite of first account"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.tokens[0].native(&bank).round(),
|
||||||
|
I80F48::from(initial_token_deposit) + partial_settle_amount,
|
||||||
|
"account 0 token native position increased (profit) by max_settle_amount"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_1.tokens[0].native(&bank).round(),
|
||||||
|
I80F48::from(initial_token_deposit) - partial_settle_amount,
|
||||||
|
"account 1 token native position decreased (loss) by max_settle_amount"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.net_settled, partial_settle_amount,
|
||||||
|
"net_settled on account 0 updated with profit from settlement"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_1.net_settled, -partial_settle_amount,
|
||||||
|
"net_settled on account 1 updated with loss from settlement"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
solana.advance_clock().await;
|
||||||
|
|
||||||
|
// Fully execute the settle
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpSettlePnlInstruction {
|
||||||
|
group,
|
||||||
|
account_a: account_0,
|
||||||
|
account_b: account_1,
|
||||||
|
perp_market,
|
||||||
|
oracle: tokens[0].oracle,
|
||||||
|
quote_bank: tokens[0].bank,
|
||||||
|
max_settle_amount: I80F48::MAX,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||||
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.perps[0].base_position_lots, 1,
|
||||||
|
"base position unchanged for account 0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_1.perps[0].base_position_lots, -1,
|
||||||
|
"base position unchanged for account 1"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.perps[0].quote_position_native.round(),
|
||||||
|
I80F48::from(-100_020) - expected_pnl_0,
|
||||||
|
"quote position reduced for profitable position"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_1.perps[0].quote_position_native.round(),
|
||||||
|
I80F48::from(100_000) + expected_pnl_0,
|
||||||
|
"quote position increased for losing position by opposite of first account"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.tokens[0].native(&bank).round(),
|
||||||
|
I80F48::from(initial_token_deposit) + expected_pnl_0,
|
||||||
|
"account 0 token native position increased (profit)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_1.tokens[0].native(&bank).round(),
|
||||||
|
I80F48::from(initial_token_deposit) - expected_pnl_0,
|
||||||
|
"account 1 token native position decreased (loss)"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.net_settled, expected_pnl_0,
|
||||||
|
"net_settled on account 0 updated with profit from settlement"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_1.net_settled, -expected_pnl_0,
|
||||||
|
"net_settled on account 1 updated with loss from settlement"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
solana.advance_clock().await;
|
||||||
|
|
||||||
|
// Change the oracle to a reasonable price in other direction
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
StubOracleSetInstruction {
|
||||||
|
group,
|
||||||
|
admin,
|
||||||
|
mint: mints[0].pubkey,
|
||||||
|
payer,
|
||||||
|
price: "995.0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let expected_pnl_0 = I80F48::from(-1000);
|
||||||
|
let expected_pnl_1 = I80F48::from(980);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
|
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
|
||||||
|
assert_eq!(
|
||||||
|
get_pnl_native(&mango_account_0.perps[0], &perp_market, I80F48::from(995)).round(),
|
||||||
|
expected_pnl_0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_pnl_native(&mango_account_1.perps[0], &perp_market, I80F48::from(995)).round(),
|
||||||
|
expected_pnl_1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
solana.advance_clock().await;
|
||||||
|
|
||||||
|
// Fully execute the settle
|
||||||
|
send_tx(
|
||||||
|
solana,
|
||||||
|
PerpSettlePnlInstruction {
|
||||||
|
group,
|
||||||
|
account_a: account_1,
|
||||||
|
account_b: account_0,
|
||||||
|
perp_market,
|
||||||
|
oracle: tokens[0].oracle,
|
||||||
|
quote_bank: tokens[0].bank,
|
||||||
|
max_settle_amount: I80F48::MAX,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
|
||||||
|
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
|
||||||
|
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.perps[0].base_position_lots, 1,
|
||||||
|
"base position unchanged for account 0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_1.perps[0].base_position_lots, -1,
|
||||||
|
"base position unchanged for account 1"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.perps[0].quote_position_native.round(),
|
||||||
|
I80F48::from(-100_500) + expected_pnl_1,
|
||||||
|
"quote position increased for losing position"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_1.perps[0].quote_position_native.round(),
|
||||||
|
I80F48::from(100_480) - expected_pnl_1,
|
||||||
|
"quote position reduced for losing position by opposite of first account"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 480 was previous settlement
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.tokens[0].native(&bank).round(),
|
||||||
|
I80F48::from(initial_token_deposit + 480) - expected_pnl_1,
|
||||||
|
"account 0 token native position decreased (loss)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_1.tokens[0].native(&bank).round(),
|
||||||
|
I80F48::from(initial_token_deposit - 480) + expected_pnl_1,
|
||||||
|
"account 1 token native position increased (profit)"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_0.net_settled,
|
||||||
|
I80F48::from(480) - expected_pnl_1,
|
||||||
|
"net_settled on account 0 updated with loss from settlement"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mango_account_1.net_settled,
|
||||||
|
I80F48::from(-480) + expected_pnl_1,
|
||||||
|
"net_settled on account 1 updated with profit from settlement"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_pnl_native(
|
||||||
|
perp_position: &PerpPosition,
|
||||||
|
perp_market: &PerpMarket,
|
||||||
|
oracle_price: I80F48,
|
||||||
|
) -> I80F48 {
|
||||||
|
let contract_size = perp_market.base_lot_size;
|
||||||
|
let new_quote_pos =
|
||||||
|
I80F48::from_num(-perp_position.base_position_lots * contract_size) * oracle_price;
|
||||||
|
perp_position.quote_position_native - new_quote_pos
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_mango_error<T>(result: &Result<T, TransportError>, expected_error: u32, comment: String) {
|
||||||
|
match result {
|
||||||
|
Ok(_) => assert!(false, "No error returned"),
|
||||||
|
Err(TransportError::TransactionError(tx_err)) => match tx_err {
|
||||||
|
TransactionError::InstructionError(_, err) => match err {
|
||||||
|
InstructionError::Custom(err_num) => {
|
||||||
|
assert_eq!(*err_num, expected_error, "{}", comment);
|
||||||
|
}
|
||||||
|
_ => assert!(false, "Not a mango error"),
|
||||||
|
},
|
||||||
|
_ => assert!(false, "Not a mango error"),
|
||||||
|
},
|
||||||
|
_ => assert!(false, "Not a mango error"),
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue