Perps: introduce explicit position close (#216)

* Perps: fix position lifetime; explicit closing

When an order is placed and the position needs to be created, the
settlement token is marked as in use. The perp position and the in-use
flag are only released with the new perp_close_position instruction.

* Tests: Factor out common floating-point comparisons

* Rename PerpClosePosition -> PerpDeactivatePosition
This commit is contained in:
Christian Kamm 2022-09-09 10:50:09 +02:00 committed by GitHub
parent 6c32077d1a
commit 1f26a54399
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 340 additions and 54 deletions

View File

@ -18,6 +18,7 @@ pub use perp_cancel_order_by_client_order_id::*;
pub use perp_close_market::*;
pub use perp_consume_events::*;
pub use perp_create_market::*;
pub use perp_deactivate_position::*;
pub use perp_edit_market::*;
pub use perp_place_order::*;
pub use perp_settle_fees::*;
@ -64,6 +65,7 @@ mod perp_cancel_order_by_client_order_id;
mod perp_close_market;
mod perp_consume_events;
mod perp_create_market;
mod perp_deactivate_position;
mod perp_edit_market;
mod perp_place_order;
mod perp_settle_fees;

View File

@ -0,0 +1,61 @@
use anchor_lang::prelude::*;
use crate::error::*;
use crate::state::*;
use crate::util::checked_math as cm;
#[derive(Accounts)]
pub struct PerpDeactivatePosition<'info> {
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group
// owner is checked at #1
)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(has_one = group)]
pub perp_market: AccountLoader<'info, PerpMarket>,
}
pub fn perp_deactivate_position(ctx: Context<PerpDeactivatePosition>) -> Result<()> {
let mut account = ctx.accounts.account.load_mut()?;
// account constraint #1
require!(
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
MangoError::SomeError
);
let perp_market = ctx.accounts.perp_market.load()?;
let perp_position = account.perp_position_mut(perp_market.perp_market_index)?;
// Is the perp position closable?
perp_position.settle_funding(&perp_market);
require_msg!(
perp_position.base_position_lots() == 0,
"perp position still has base lots"
);
// No dusting needed because we're able to use settle_pnl to get this to 0.
require_msg!(
perp_position.quote_position_native() == 0,
"perp position still has quote position"
);
require_msg!(
perp_position.bids_base_lots == 0 && perp_position.asks_base_lots == 0,
"perp position still has open orders"
);
require_msg!(
perp_position.taker_base_lots == 0 && perp_position.taker_quote_lots == 0,
"perp position still has events on event queue"
);
account.deactivate_perp_position(perp_market.perp_market_index)?;
// Reduce the in-use-count of the settlement token
let mut token_position = account.token_position_mut(QUOTE_TOKEN_INDEX)?.0;
cm!(token_position.in_use_count -= 1);
Ok(())
}

View File

@ -5,8 +5,9 @@ use crate::error::*;
use crate::state::MangoAccount;
use crate::state::{
new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, Book, BookSide,
EventQueue, Group, OrderType, PerpMarket, Side,
EventQueue, Group, OrderType, PerpMarket, Side, QUOTE_TOKEN_INDEX,
};
use crate::util::checked_math as cm;
#[derive(Accounts)]
pub struct PerpPlaceOrder<'info> {
@ -83,11 +84,21 @@ pub fn perp_place_order(
let account_pk = ctx.accounts.account.key();
let perp_market_index = {
let perp_market = ctx.accounts.perp_market.load()?;
perp_market.perp_market_index
};
let (_, perp_position_raw_index) = account.ensure_perp_position(perp_market_index)?;
let perp_market_index = ctx.accounts.perp_market.load()?.perp_market_index;
//
// Create the perp position if needed
//
if !account
.active_perp_positions()
.any(|p| p.is_active_for_market(perp_market_index))
{
account.ensure_perp_position(perp_market_index)?;
// Require that the token position for the settlement token is retained
let mut token_position = account.ensure_token_position(QUOTE_TOKEN_INDEX)?.0;
cm!(token_position.in_use_count += 1);
}
//
// Pre-health computation, _after_ perp position is created
@ -151,7 +162,7 @@ pub fn perp_place_order(
// Health check
//
if let Some((mut health_cache, pre_health)) = pre_health_opt {
let perp_position = account.perp_position_by_raw_index(perp_position_raw_index);
let perp_position = account.perp_position(perp_market_index)?;
health_cache.recompute_perp_info(perp_position, &perp_market)?;
account.check_health_post(&health_cache, pre_health)?;
}

View File

@ -439,6 +439,10 @@ pub mod mango_v4 {
// TODO perp_change_perp_market_params
pub fn perp_deactivate_position(ctx: Context<PerpDeactivatePosition>) -> Result<()> {
instructions::perp_deactivate_position(ctx)
}
#[allow(clippy::too_many_arguments)]
pub fn perp_place_order(
ctx: Context<PerpPlaceOrder>,

View File

@ -724,8 +724,9 @@ impl<
}
}
pub fn deactivate_perp_position(&mut self, raw_index: usize) {
self.perp_position_mut_by_raw_index(raw_index).market_index = PerpMarketIndex::MAX;
pub fn deactivate_perp_position(&mut self, perp_market_index: PerpMarketIndex) -> Result<()> {
self.perp_position_mut(perp_market_index)?.market_index = PerpMarketIndex::MAX;
Ok(())
}
pub fn add_perp_order(
@ -734,8 +735,7 @@ impl<
side: Side,
order: &LeafNode,
) -> Result<()> {
// TODO: pass in the PerpPosition, currently has a creation side-effect
let mut perp_account = self.ensure_perp_position(perp_market_index).unwrap().0;
let mut perp_account = self.perp_position_mut(perp_market_index)?;
match side {
Side::Bid => {
cm!(perp_account.bids_base_lots += order.quantity);
@ -755,13 +755,12 @@ impl<
}
pub fn remove_perp_order(&mut self, slot: usize, quantity: i64) -> Result<()> {
// TODO: pass in the PerpPosition, currently has a creation side-effect
{
let oo = self.perp_order_mut_by_raw_index(slot);
require_neq!(oo.order_market, FREE_ORDER_SLOT);
let order_side = oo.order_side;
let perp_market_index = oo.order_market;
let perp_account = self.ensure_perp_position(perp_market_index).unwrap().0;
let perp_account = self.perp_position_mut(perp_market_index)?;
// accounting
match order_side {
@ -789,8 +788,7 @@ impl<
perp_market: &mut PerpMarket,
fill: &FillEvent,
) -> Result<()> {
// TODO: pass in the PerpPosition, currently has a creation side-effect
let pa = self.ensure_perp_position(perp_market_index).unwrap().0;
let pa = self.perp_position_mut(perp_market_index)?;
pa.settle_funding(perp_market);
let side = fill.taker_side.invert_side();
@ -824,8 +822,7 @@ impl<
perp_market: &mut PerpMarket,
fill: &FillEvent,
) -> Result<()> {
// TODO: pass in the PerpPosition, currently has a creation side-effect
let pa = self.ensure_perp_position(perp_market_index).unwrap().0;
let pa = self.perp_position_mut(perp_market_index)?;
pa.settle_funding(perp_market);
let (base_change, quote_change) = fill.base_quote_change(fill.taker_side);
@ -1188,7 +1185,7 @@ mod tests {
fn test_perp_positions() {
let mut account = make_test_account();
assert!(account.perp_position(1).is_err());
//assert!(account.perp_position_mut(3).is_err());
assert!(account.perp_position_mut(3).is_err());
assert_eq!(
account.perp_position_by_raw_index(0).market_index,
PerpMarketIndex::MAX
@ -1222,7 +1219,7 @@ mod tests {
}
{
account.deactivate_perp_position(1);
assert!(account.deactivate_perp_position(7).is_ok());
let (pos, raw) = account.ensure_perp_position(42).unwrap();
assert_eq!(raw, 2);
@ -1234,13 +1231,13 @@ mod tests {
}
assert_eq!(account.active_perp_positions().count(), 3);
account.deactivate_perp_position(0);
assert!(account.deactivate_perp_position(1).is_ok());
assert_eq!(
account.perp_position_by_raw_index(0).market_index,
PerpMarketIndex::MAX
);
assert!(account.perp_position(1).is_err());
//assert!(account.perp_position_mut(1).is_err());
assert!(account.perp_position_mut(1).is_err());
assert!(account.perp_position(8).is_ok());
assert!(account.perp_position(42).is_ok());
assert_eq!(account.active_perp_positions().count(), 2);

View File

@ -263,9 +263,7 @@ impl<'a> Book<'a> {
// Record the taker trade in the account already, even though it will only be
// realized when the fill event gets executed
let perp_account = mango_account
.ensure_perp_position(market.perp_market_index)?
.0;
let perp_account = mango_account.perp_position_mut(market.perp_market_index)?;
perp_account.add_taker_trade(side, match_base_lots, match_quote_lots);
let fill = FillEvent::new(
@ -465,9 +463,7 @@ fn apply_fees(
let maker_fees = taker_quote_native * market.maker_fee;
let taker_fees = taker_quote_native * market.taker_fee;
let perp_account = mango_account
.ensure_perp_position(market.perp_market_index)?
.0;
let perp_account = mango_account.perp_position_mut(market.perp_market_index)?;
perp_account.change_quote_position(-taker_fees);
market.fees_accrued += taker_fees + maker_fees;

View File

@ -103,6 +103,9 @@ mod tests {
|book: &mut Book, event_queue: &mut EventQueue, side, price, now_ts| -> i128 {
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
account
.ensure_perp_position(perp_market.perp_market_index)
.unwrap();
let quantity = 1;
let tif = 100;
@ -199,6 +202,12 @@ mod tests {
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut maker = MangoAccountValue::from_bytes(&buffer).unwrap();
let mut taker = MangoAccountValue::from_bytes(&buffer).unwrap();
maker
.ensure_perp_position(market.perp_market_index)
.unwrap();
taker
.ensure_perp_position(market.perp_market_index)
.unwrap();
let maker_pk = Pubkey::new_unique();
let taker_pk = Pubkey::new_unique();

View File

@ -2220,6 +2220,39 @@ impl ClientInstruction for PerpCloseMarketInstruction {
}
}
pub struct PerpDeactivatePositionInstruction {
pub account: Pubkey,
pub perp_market: Pubkey,
pub owner: TestKeypair,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpDeactivatePositionInstruction {
type Accounts = mango_v4::accounts::PerpDeactivatePosition;
type Instruction = mango_v4::instruction::PerpDeactivatePosition;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let instruction = Self::Instruction {};
let accounts = Self::Accounts {
group: perp_market.group,
account: self.account,
perp_market: self.perp_market,
owner: self.owner.pubkey(),
};
let instruction = make_instruction(program_id, &accounts, instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.owner]
}
}
pub struct PerpPlaceOrderInstruction {
pub group: Pubkey,
pub account: Pubkey,

View File

@ -111,3 +111,19 @@ pub fn assert_mango_error<T>(
_ => assert!(false, "Not a mango error"),
}
}
pub fn assert_equal_fixed_f64(value: I80F48, expected: f64, max_error: f64) -> bool {
let ok = (value.to_num::<f64>() - expected).abs() < max_error;
if !ok {
println!("comparison failed: value: {value}, expected: {expected}");
}
ok
}
pub fn assert_equal_f64_f64(value: f64, expected: f64, max_error: f64) -> bool {
let ok = (value - expected).abs() < max_error;
if !ok {
println!("comparison failed: value: {value}, expected: {expected}");
}
ok
}

View File

@ -23,7 +23,7 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
let loan_origination_fee = 0.0005;
// higher resolution that the loan_origination_fee for one token
let balance_f64eq = |a: f64, b: f64| (a - b).abs() < 0.0001;
let balance_f64eq = |a: f64, b: f64| utils::assert_equal_f64_f64(a, b, 0.0001);
//
// SETUP: Create a group, account, register a token (mint0)

View File

@ -1,6 +1,7 @@
#![cfg(all(feature = "test-bpf"))]
use anchor_lang::prelude::Pubkey;
use fixed::types::I80F48;
use fixed_macro::types::I80F48;
use mango_v4::state::*;
use program_test::*;
@ -8,6 +9,7 @@ use solana_program_test::*;
use solana_sdk::transport::TransportError;
use mango_setup::*;
use utils::assert_equal_fixed_f64 as assert_equal;
mod program_test;
@ -97,8 +99,8 @@ async fn test_perp() -> Result<(), TransportError> {
maint_liab_weight: 1.025,
init_liab_weight: 1.05,
liquidation_fee: 0.012,
maker_fee: 0.0002,
taker_fee: 0.000,
maker_fee: -0.0001,
taker_fee: 0.0002,
},
)
.await
@ -342,12 +344,172 @@ async fn test_perp() -> Result<(), TransportError> {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
assert!(mango_account_0.perps[0].quote_position_native() < -100.019);
assert!(assert_equal(
mango_account_0.perps[0].quote_position_native(),
-99.99,
0.001
));
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
assert_eq!(mango_account_1.perps[0].quote_position_native(), 100);
assert!(assert_equal(
mango_account_1.perps[0].quote_position_native(),
99.98,
0.001
));
//
// TEST: closing perp positions
//
// Can't close yet, active positions
assert!(send_tx(
solana,
PerpDeactivatePositionInstruction {
account: account_0,
perp_market,
owner,
},
)
.await
.is_err());
solana.advance_by_slots(1).await;
// Trade again to bring base_position_lots to 0
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_0,
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: 7,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
group,
account: account_1,
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: 8,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_1).await;
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;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 0);
assert!(assert_equal(
mango_account_0.perps[0].quote_position_native(),
0.02,
0.001
));
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), 0);
assert!(assert_equal(
mango_account_1.perps[0].quote_position_native(),
-0.04,
0.001
));
// settle pnl and fees to bring quote_position_native fully to 0
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();
send_tx(
solana,
PerpSettleFeesInstruction {
group,
account: account_1,
perp_market,
oracle: tokens[0].oracle,
quote_bank: tokens[0].bank,
max_settle_amount: I80F48::MAX,
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].quote_position_native(), 0);
// Now closing works!
send_tx(
solana,
PerpDeactivatePositionInstruction {
account: account_0,
perp_market,
owner,
},
)
.await
.unwrap();
send_tx(
solana,
PerpDeactivatePositionInstruction {
account: account_1,
perp_market,
owner,
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
assert_eq!(mango_account_0.perps[0].market_index, PerpMarketIndex::MAX);
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].market_index, PerpMarketIndex::MAX);
//
// TEST: market closing (testing only)
//
send_tx(
solana,
PerpCloseMarketInstruction {

View File

@ -6,6 +6,7 @@ use solana_sdk::transport::TransportError;
use mango_setup::*;
use program_test::*;
use utils::assert_equal_fixed_f64 as assert_equal;
mod program_test;
@ -86,28 +87,22 @@ async fn test_token_update_index_and_rate() -> Result<(), TransportError> {
let interest_change = 5000.0 * (dynamic_rate + loan_fee_rate) * diff_ts / year;
let fee_change = 5000.0 * loan_fee_rate * diff_ts / year;
assert!(
(bank_after.native_borrows().to_num::<f64>()
- bank_before.native_borrows().to_num::<f64>()
- interest_change)
.abs()
< 0.1
);
assert!(
(bank_after.native_deposits().to_num::<f64>()
- bank_before.native_deposits().to_num::<f64>()
- interest_change)
.abs()
< 0.1
);
assert!(
(bank_after.collected_fees_native.to_num::<f64>()
- bank_before.collected_fees_native.to_num::<f64>()
- fee_change)
.abs()
< 0.1
);
assert!((bank_after.avg_utilization.to_num::<f64>() - utilization).abs() < 0.01);
assert!(assert_equal(
bank_after.native_borrows() - bank_before.native_borrows(),
interest_change,
0.1
));
assert!(assert_equal(
bank_after.native_deposits() - bank_before.native_deposits(),
interest_change,
0.1
));
assert!(assert_equal(
bank_after.collected_fees_native - bank_before.collected_fees_native,
fee_change,
0.1
));
assert!(assert_equal(bank_after.avg_utilization, utilization, 0.01));
Ok(())
}