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:
parent
6c32077d1a
commit
1f26a54399
|
@ -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;
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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)?;
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue