mango-v4/programs/mango-v4/tests/cases/test_perp.rs

1050 lines
26 KiB
Rust

use super::*;
#[tokio::test]
async fn test_perp_fixed() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..2];
//
// SETUP: Create a group and an account
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let deposit_amount = 1000;
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
let settler =
create_funded_account(&solana, group, owner, 251, &context.users[1], &[], 0, 0).await;
let settler_owner = owner.clone();
//
// TEST: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket {
perp_market, bids, ..
} = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
payer,
perp_market_index: 0,
quote_lot_size: 10,
base_lot_size: 100,
maint_base_asset_weight: 0.975,
init_base_asset_weight: 0.95,
maint_base_liab_weight: 1.025,
init_base_liab_weight: 1.05,
base_liquidation_fee: 0.012,
maker_fee: -0.0001,
taker_fee: 0.0002,
settle_pnl_limit_factor: -1.0,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
},
)
.await
.unwrap();
let price_lots = {
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
perp_market.native_price_to_lot(I80F48::ONE)
};
//
// Place and cancel order with order_id
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 1,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
let bids_data = solana.get_account_boxed::<BookSide>(bids).await;
assert_eq!(bids_data.roots[0].leaf_count, 1);
let order_id_to_cancel = solana
.get_account::<MangoAccount>(account_0)
.await
.perp_open_orders[0]
.id;
send_tx(
solana,
PerpCancelOrderInstruction {
account: account_0,
perp_market,
owner,
order_id: order_id_to_cancel,
},
)
.await
.unwrap();
assert_no_perp_orders(solana, account_0).await;
//
// Place and cancel order with client_order_id
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 1,
client_order_id: 1,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpCancelOrderByClientOrderIdInstruction {
account: account_0,
perp_market,
owner,
client_order_id: 1,
},
)
.await
.unwrap();
assert_no_perp_orders(solana, account_0).await;
//
// Place and cancel all orders
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 1,
client_order_id: 2,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderPeggedInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_offset: -1,
peg_limit: -1,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 3,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 1,
client_order_id: 4,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpCancelAllOrdersInstruction {
account: account_0,
perp_market,
owner,
},
)
.await
.unwrap();
assert_no_perp_orders(solana, account_0).await;
//
// Place a bid, corresponding ask, and consume event
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 1,
client_order_id: 5,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 1,
client_order_id: 6,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_1).await;
// Trying to cancel-all after the order was already taken: has no effect but succeeds
send_tx(
solana,
PerpCancelAllOrdersInstruction {
account: account_0,
perp_market,
owner,
},
)
.await
.unwrap();
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
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(), 1);
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!(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 {
account: account_0,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 1,
client_order_id: 7,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 1,
client_order_id: 8,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_1).await;
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
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 {
settler,
settler_owner,
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank: tokens[0].bank,
},
)
.await
.unwrap();
send_tx(
solana,
PerpSettleFeesInstruction {
account: account_1,
perp_market,
settle_bank: tokens[0].bank,
max_settle_amount: u64::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 {
admin,
perp_market,
sol_destination: payer.pubkey(),
},
)
.await
.unwrap();
Ok(())
}
#[tokio::test]
async fn test_perp_oracle_peg() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..2];
//
// SETUP: Create a group and an account
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let deposit_amount = 100000;
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
//
// SETUP: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket {
perp_market, bids, ..
} = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
payer,
perp_market_index: 0,
quote_lot_size: 10,
base_lot_size: 10000,
maint_base_asset_weight: 0.975,
init_base_asset_weight: 0.95,
maint_base_liab_weight: 1.025,
init_base_liab_weight: 1.05,
base_liquidation_fee: 0.012,
maker_fee: -0.0001,
taker_fee: 0.0002,
settle_pnl_limit_factor: 0.2,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[0]).await
},
)
.await
.unwrap();
let price_lots = {
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
perp_market.native_price_to_lot(I80F48::ONE)
};
assert_eq!(price_lots, 1000);
//
// TEST: Place and cancel order with order_id
//
send_tx(
solana,
PerpPlaceOrderPeggedInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_offset: -1,
peg_limit: -1,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
let bids_data = solana.get_account_boxed::<BookSide>(bids).await;
assert_eq!(bids_data.roots[1].leaf_count, 1);
let perp_order = solana
.get_account::<MangoAccount>(account_0)
.await
.perp_open_orders[0];
assert_eq!(
perp_order.side_and_tree(),
SideAndOrderTree::BidOraclePegged
);
send_tx(
solana,
PerpCancelOrderInstruction {
account: account_0,
perp_market,
owner,
order_id: perp_order.id,
},
)
.await
.unwrap();
assert_no_perp_orders(solana, account_0).await;
//
// TEST: Place a pegged bid, take it with a direct and pegged ask, and consume events
//
send_tx(
solana,
PerpPlaceOrderPeggedInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_offset: 0,
peg_limit: -1,
max_base_lots: 2,
max_quote_lots: i64::MAX,
client_order_id: 5,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 1,
client_order_id: 6,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_1).await;
send_tx(
solana,
PerpPlaceOrderPeggedInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_offset: 0,
peg_limit: -1,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 7,
},
)
.await
.unwrap();
check_prev_instruction_post_health(&solana, account_1).await;
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
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(), 2);
assert!(assert_equal(
mango_account_0.perps[0].quote_position_native(),
-19998.0,
0.001
));
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_1.perps[0].base_position_lots(), -2);
assert!(assert_equal(
mango_account_1.perps[0].quote_position_native(),
19996.0,
0.001
));
//
// TEST: Place a pegged order and check how it behaves with oracle changes
//
send_tx(
solana,
PerpPlaceOrderPeggedInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_offset: -1,
peg_limit: -1,
max_base_lots: 2,
max_quote_lots: i64::MAX,
client_order_id: 5,
},
)
.await
.unwrap();
// TEST: an ask at current oracle price does not match
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 1,
client_order_id: 60,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
send_tx(
solana,
PerpCancelOrderByClientOrderIdInstruction {
account: account_1,
perp_market,
owner,
client_order_id: 60,
},
)
.await
.unwrap();
// TEST: Change the oracle, now the ask matches
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[0], admin, 1.002).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 2,
client_order_id: 61,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
assert_no_perp_orders(solana, account_0).await;
// restore the oracle to default
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[0], admin, 1.0).await;
//
// TEST: order is cancelled when the price exceeds the peg limit
//
send_tx(
solana,
PerpPlaceOrderPeggedInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_offset: -1,
peg_limit: price_lots + 2,
max_base_lots: 2,
max_quote_lots: i64::MAX,
client_order_id: 5,
},
)
.await
.unwrap();
// order is still matchable when exactly at the peg limit
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[0], admin, 1.003).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots: price_lots + 2,
max_base_lots: 1,
client_order_id: 62,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
assert!(send_tx(
solana,
PerpCancelOrderByClientOrderIdInstruction {
account: account_1,
perp_market,
owner,
client_order_id: 62,
},
)
.await
.is_err());
// but once the adjusted price is > the peg limit, it's gone
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[0], admin, 1.004).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots: price_lots + 3,
max_base_lots: 1,
client_order_id: 63,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
send_tx(
solana,
PerpCancelOrderByClientOrderIdInstruction {
account: account_1,
perp_market,
owner,
client_order_id: 63,
},
)
.await
.unwrap();
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
assert_no_perp_orders(solana, account_0).await;
Ok(())
}
#[tokio::test]
async fn test_perp_realize_partially() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..2];
//
// SETUP: Create a group and an account
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let deposit_amount = 1000;
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
mints,
deposit_amount,
0,
)
.await;
//
// TEST: Create a perp market
//
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
payer,
perp_market_index: 0,
quote_lot_size: 10,
base_lot_size: 100,
maint_base_asset_weight: 0.975,
init_base_asset_weight: 0.95,
maint_base_liab_weight: 1.025,
init_base_liab_weight: 1.05,
base_liquidation_fee: 0.012,
maker_fee: 0.0000,
taker_fee: 0.0000,
settle_pnl_limit_factor: -1.0,
settle_pnl_limit_window_size_ts: 24 * 60 * 60,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
},
)
.await
.unwrap();
let perp_market_data = solana.get_account::<PerpMarket>(perp_market).await;
let price_lots = perp_market_data.native_price_to_lot(I80F48::from(1000));
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1000.0).await;
//
// SETUP: Place a bid, corresponding ask, and consume event
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 2,
client_order_id: 5,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 2,
client_order_id: 6,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let perp_0 = mango_account_0.perps[0];
assert_eq!(perp_0.base_position_lots(), 2);
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let perp_1 = mango_account_1.perps[0];
assert_eq!(perp_1.base_position_lots(), -2);
//
// SETUP: Sell one lot again at increased price
//
set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1500.0).await;
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Ask,
price_lots: perp_market_data.native_price_to_lot(I80F48::from_num(1500)),
max_base_lots: 1,
client_order_id: 5,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Bid,
price_lots: perp_market_data.native_price_to_lot(I80F48::from_num(1500)),
max_base_lots: 1,
client_order_id: 6,
..PerpPlaceOrderInstruction::default()
},
)
.await
.unwrap();
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let perp_0 = mango_account_0.perps[0];
assert_eq!(perp_0.base_position_lots(), 1);
assert!(assert_equal(
perp_0.quote_position_native(),
-200_000.0 + 150_000.0,
0.001
));
assert!(assert_equal(
perp_0.realized_pnl_for_position_native,
50_000.0,
0.001
));
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
let perp_1 = mango_account_1.perps[0];
assert_eq!(perp_1.base_position_lots(), -1);
assert!(assert_equal(
perp_1.quote_position_native(),
200_000.0 - 150_000.0,
0.001
));
assert!(assert_equal(
perp_1.realized_pnl_for_position_native,
-50_000.0,
0.001
));
Ok(())
}
async fn assert_no_perp_orders(solana: &SolanaCookie, account_0: Pubkey) {
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
for oo in mango_account_0.perp_open_orders.iter() {
assert!(oo.id == 0);
assert!(oo.side_and_tree() == SideAndOrderTree::BidFixed);
assert!(oo.client_id == 0);
assert!(oo.market == FREE_ORDER_SLOT);
}
}