Merge pull request #234 from blockworks-foundation/cj/ioc_fees
Extra fees for IOC orders
This commit is contained in:
commit
a97b40a521
|
@ -64,6 +64,7 @@ pub fn perp_create_market(
|
|||
impact_quantity: i64,
|
||||
group_insurance_fund: bool,
|
||||
trusted_market: bool,
|
||||
fee_penalty: f32,
|
||||
) -> Result<()> {
|
||||
let mut perp_market = ctx.accounts.perp_market.load_init()?;
|
||||
*perp_market = PerpMarket {
|
||||
|
@ -103,7 +104,8 @@ pub fn perp_create_market(
|
|||
padding0: Default::default(),
|
||||
padding1: Default::default(),
|
||||
padding2: Default::default(),
|
||||
reserved: [0; 112],
|
||||
fee_penalty,
|
||||
reserved: [0; 108],
|
||||
};
|
||||
|
||||
let mut bids = ctx.accounts.bids.load_init()?;
|
||||
|
|
|
@ -35,6 +35,7 @@ pub fn perp_edit_market(
|
|||
impact_quantity_opt: Option<i64>,
|
||||
group_insurance_fund_opt: Option<bool>,
|
||||
trusted_market_opt: Option<bool>,
|
||||
fee_penalty_opt: Option<f32>,
|
||||
) -> Result<()> {
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
|
||||
|
@ -91,6 +92,9 @@ pub fn perp_edit_market(
|
|||
if let Some(impact_quantity) = impact_quantity_opt {
|
||||
perp_market.impact_quantity = impact_quantity;
|
||||
}
|
||||
if let Some(fee_penalty) = fee_penalty_opt {
|
||||
perp_market.fee_penalty = fee_penalty;
|
||||
}
|
||||
|
||||
// unchanged -
|
||||
// long_funding
|
||||
|
|
|
@ -393,6 +393,7 @@ pub mod mango_v4 {
|
|||
impact_quantity: i64,
|
||||
group_insurance_fund: bool,
|
||||
trusted_market: bool,
|
||||
fee_penalty: f32,
|
||||
) -> Result<()> {
|
||||
instructions::perp_create_market(
|
||||
ctx,
|
||||
|
@ -414,6 +415,7 @@ pub mod mango_v4 {
|
|||
impact_quantity,
|
||||
group_insurance_fund,
|
||||
trusted_market,
|
||||
fee_penalty,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -435,6 +437,7 @@ pub mod mango_v4 {
|
|||
impact_quantity_opt: Option<i64>,
|
||||
group_insurance_fund_opt: Option<bool>,
|
||||
trusted_market_opt: Option<bool>,
|
||||
fee_penalty_opt: Option<f32>,
|
||||
) -> Result<()> {
|
||||
instructions::perp_edit_market(
|
||||
ctx,
|
||||
|
@ -453,6 +456,7 @@ pub mod mango_v4 {
|
|||
impact_quantity_opt,
|
||||
group_insurance_fund_opt,
|
||||
trusted_market_opt,
|
||||
fee_penalty_opt,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -400,8 +400,7 @@ pub use account_seeds;
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state::{OracleConfig, PerpMarket};
|
||||
use anchor_lang::prelude::Pubkey;
|
||||
use crate::state::PerpMarket;
|
||||
use fixed::types::I80F48;
|
||||
use rand::Rng;
|
||||
|
||||
|
@ -416,52 +415,9 @@ mod tests {
|
|||
pos
|
||||
}
|
||||
|
||||
fn create_perp_market() -> PerpMarket {
|
||||
return PerpMarket {
|
||||
group: Pubkey::new_unique(),
|
||||
perp_market_index: 0,
|
||||
group_insurance_fund: 0,
|
||||
trusted_market: 0,
|
||||
name: Default::default(),
|
||||
oracle: Pubkey::new_unique(),
|
||||
oracle_config: OracleConfig {
|
||||
conf_filter: I80F48::ZERO,
|
||||
},
|
||||
bids: Pubkey::new_unique(),
|
||||
asks: Pubkey::new_unique(),
|
||||
event_queue: Pubkey::new_unique(),
|
||||
quote_lot_size: 1,
|
||||
base_lot_size: 1,
|
||||
maint_asset_weight: I80F48::from(1),
|
||||
init_asset_weight: I80F48::from(1),
|
||||
maint_liab_weight: I80F48::from(1),
|
||||
init_liab_weight: I80F48::from(1),
|
||||
liquidation_fee: I80F48::ZERO,
|
||||
maker_fee: I80F48::ZERO,
|
||||
taker_fee: I80F48::ZERO,
|
||||
min_funding: I80F48::ZERO,
|
||||
max_funding: I80F48::ZERO,
|
||||
impact_quantity: 0,
|
||||
long_funding: I80F48::ZERO,
|
||||
short_funding: I80F48::ZERO,
|
||||
funding_last_updated: 0,
|
||||
open_interest: 0,
|
||||
seq_num: 0,
|
||||
fees_accrued: I80F48::ZERO,
|
||||
fees_settled: I80F48::ZERO,
|
||||
bump: 0,
|
||||
base_decimals: 0,
|
||||
reserved: [0; 112],
|
||||
padding0: Default::default(),
|
||||
padding1: Default::default(),
|
||||
padding2: Default::default(),
|
||||
registration_time: 0,
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quote_entry_long_increasing_from_zero() {
|
||||
let mut market = create_perp_market();
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(0, 0, 0);
|
||||
// Go long 10 @ 10
|
||||
pos.change_base_and_quote_positions(&mut market, 10, I80F48::from(-100));
|
||||
|
@ -472,7 +428,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_quote_entry_short_increasing_from_zero() {
|
||||
let mut market = create_perp_market();
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(0, 0, 0);
|
||||
// Go short 10 @ 10
|
||||
pos.change_base_and_quote_positions(&mut market, -10, I80F48::from(100));
|
||||
|
@ -483,7 +439,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_quote_entry_long_increasing_from_long() {
|
||||
let mut market = create_perp_market();
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(10, -100, -100);
|
||||
// Go long 10 @ 30
|
||||
pos.change_base_and_quote_positions(&mut market, 10, I80F48::from(-300));
|
||||
|
@ -494,7 +450,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_quote_entry_short_increasing_from_short() {
|
||||
let mut market = create_perp_market();
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(-10, 100, 100);
|
||||
// Go short 10 @ 10
|
||||
pos.change_base_and_quote_positions(&mut market, -10, I80F48::from(300));
|
||||
|
@ -505,7 +461,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_quote_entry_long_decreasing_from_short() {
|
||||
let mut market = create_perp_market();
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(-10, 100, 100);
|
||||
// Go long 5 @ 50
|
||||
pos.change_base_and_quote_positions(&mut market, 5, I80F48::from(-250));
|
||||
|
@ -516,7 +472,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_quote_entry_short_decreasing_from_long() {
|
||||
let mut market = create_perp_market();
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(10, -100, -100);
|
||||
// Go short 5 @ 50
|
||||
pos.change_base_and_quote_positions(&mut market, -5, I80F48::from(250));
|
||||
|
@ -527,7 +483,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_quote_entry_long_close_with_short() {
|
||||
let mut market = create_perp_market();
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(10, -100, -100);
|
||||
// Go short 10 @ 50
|
||||
pos.change_base_and_quote_positions(&mut market, -10, I80F48::from(250));
|
||||
|
@ -538,7 +494,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_quote_entry_short_close_with_long() {
|
||||
let mut market = create_perp_market();
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(-10, 100, 100);
|
||||
// Go long 10 @ 50
|
||||
pos.change_base_and_quote_positions(&mut market, 10, I80F48::from(-250));
|
||||
|
@ -549,7 +505,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_quote_entry_long_close_short_with_overflow() {
|
||||
let mut market = create_perp_market();
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(10, -100, -100);
|
||||
// Go short 15 @ 20
|
||||
pos.change_base_and_quote_positions(&mut market, -15, I80F48::from(300));
|
||||
|
@ -560,7 +516,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_quote_entry_short_close_long_with_overflow() {
|
||||
let mut market = create_perp_market();
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(-10, 100, 100);
|
||||
// Go short 15 @ 20
|
||||
pos.change_base_and_quote_positions(&mut market, 15, I80F48::from(-300));
|
||||
|
@ -571,7 +527,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_quote_entry_break_even_price() {
|
||||
let mut market = create_perp_market();
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(0, 0, 0);
|
||||
// Buy 11 @ 10,000
|
||||
pos.change_base_and_quote_positions(&mut market, 11, I80F48::from(-11 * 10_000));
|
||||
|
@ -585,7 +541,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_quote_entry_multiple_and_reversed_changes_return_entry_to_zero() {
|
||||
let mut market = create_perp_market();
|
||||
let mut market = PerpMarket::default_for_tests();
|
||||
let mut pos = create_perp_position(0, 0, 0);
|
||||
|
||||
// Generate array of random trades
|
||||
|
|
|
@ -377,6 +377,11 @@ impl<'a> Book<'a> {
|
|||
apply_fees(market, mango_account, total_quote_lots_taken)?;
|
||||
}
|
||||
|
||||
// IOC orders have a fee penalty applied regardless of match
|
||||
if order_type == OrderType::ImmediateOrCancel {
|
||||
apply_penalty(market, mango_account)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -460,12 +465,24 @@ fn apply_fees(
|
|||
// risks that fees_accrued is settled to 0 before they apply. It going negative
|
||||
// breaks assumptions.
|
||||
// The maker fees apply to the maker's account only when the fill event is consumed.
|
||||
let maker_fees = taker_quote_native * market.maker_fee;
|
||||
let maker_fees = cm!(taker_quote_native * market.maker_fee);
|
||||
|
||||
let taker_fees = cm!(taker_quote_native * market.taker_fee);
|
||||
|
||||
let taker_fees = taker_quote_native * market.taker_fee;
|
||||
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;
|
||||
cm!(market.fees_accrued += taker_fees + maker_fees);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Applies a fixed penalty fee to the account, and update the market's fees_accrued
|
||||
fn apply_penalty(market: &mut PerpMarket, mango_account: &mut MangoAccountRefMut) -> Result<()> {
|
||||
let perp_account = mango_account.perp_position_mut(market.perp_market_index)?;
|
||||
let fee_penalty = I80F48::from_num(market.fee_penalty);
|
||||
|
||||
perp_account.change_quote_position(-fee_penalty);
|
||||
cm!(market.fees_accrued += fee_penalty);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -375,4 +375,111 @@ mod tests {
|
|||
match_quote - match_quote * market.taker_fee
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_penalty_applied_only_on_limit_order() -> Result<()> {
|
||||
let (mut market, oracle_price, mut event_queue, bids, asks) = test_setup(1000.0);
|
||||
let mut book = Book {
|
||||
bids: bids.borrow_mut(),
|
||||
asks: asks.borrow_mut(),
|
||||
};
|
||||
|
||||
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
|
||||
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
|
||||
let taker_pk = Pubkey::new_unique();
|
||||
let now_ts = 1000000;
|
||||
|
||||
market.base_lot_size = 1;
|
||||
market.quote_lot_size = 1;
|
||||
market.taker_fee = I80F48::from_num(0.01);
|
||||
market.fee_penalty = 5.0;
|
||||
account.ensure_perp_position(market.perp_market_index, 0)?;
|
||||
|
||||
// Passive order
|
||||
book.new_order(
|
||||
Side::Ask,
|
||||
&mut market,
|
||||
&mut event_queue,
|
||||
oracle_price,
|
||||
&mut account.borrow_mut(),
|
||||
&taker_pk,
|
||||
1000,
|
||||
2,
|
||||
i64::MAX,
|
||||
OrderType::Limit,
|
||||
0,
|
||||
43,
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Partial taker
|
||||
book.new_order(
|
||||
Side::Bid,
|
||||
&mut market,
|
||||
&mut event_queue,
|
||||
oracle_price,
|
||||
&mut account.borrow_mut(),
|
||||
&taker_pk,
|
||||
1000,
|
||||
1,
|
||||
i64::MAX,
|
||||
OrderType::Limit,
|
||||
0,
|
||||
43,
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let pos = account.perp_position(market.perp_market_index)?;
|
||||
|
||||
assert_eq!(
|
||||
pos.quote_position_native().round(),
|
||||
I80F48::from_num(-10),
|
||||
"Regular fees applied on limit order"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
market.fees_accrued.round(),
|
||||
I80F48::from_num(10),
|
||||
"Fees moved to market"
|
||||
);
|
||||
|
||||
// Full taker
|
||||
book.new_order(
|
||||
Side::Bid,
|
||||
&mut market,
|
||||
&mut event_queue,
|
||||
oracle_price,
|
||||
&mut account.borrow_mut(),
|
||||
&taker_pk,
|
||||
1000,
|
||||
1,
|
||||
i64::MAX,
|
||||
OrderType::ImmediateOrCancel,
|
||||
0,
|
||||
43,
|
||||
now_ts,
|
||||
u8::MAX,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let pos = account.perp_position(market.perp_market_index)?;
|
||||
|
||||
assert_eq!(
|
||||
pos.quote_position_native().round(),
|
||||
I80F48::from_num(-25), // -10 - 5
|
||||
"Regular fees + fixed penalty applied on IOC order"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
market.fees_accrued.round(),
|
||||
I80F48::from_num(25), // 10 + 5
|
||||
"Fees moved to market"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,12 +99,30 @@ pub struct PerpMarket {
|
|||
/// Fees settled in native quote currency
|
||||
pub fees_settled: I80F48,
|
||||
|
||||
pub reserved: [u8; 112],
|
||||
pub fee_penalty: f32,
|
||||
|
||||
pub reserved: [u8; 108],
|
||||
}
|
||||
|
||||
const_assert_eq!(
|
||||
size_of::<PerpMarket>(),
|
||||
32 + 2 + 2 + 4 + 16 + 32 + 16 + 32 * 3 + 8 * 2 + 16 * 12 + 8 * 2 + 8 * 2 + 16 + 2 + 6 + 8 + 112
|
||||
32 + 2
|
||||
+ 2
|
||||
+ 4
|
||||
+ 16
|
||||
+ 32
|
||||
+ 16
|
||||
+ 32 * 3
|
||||
+ 8 * 2
|
||||
+ 16 * 12
|
||||
+ 8 * 2
|
||||
+ 8 * 2
|
||||
+ 16
|
||||
+ 2
|
||||
+ 6
|
||||
+ 8
|
||||
+ 4
|
||||
+ 108
|
||||
);
|
||||
const_assert_eq!(size_of::<PerpMarket>() % 8, 0);
|
||||
|
||||
|
@ -225,4 +243,49 @@ impl PerpMarket {
|
|||
self.short_funding += socialized_loss;
|
||||
Ok(socialized_loss)
|
||||
}
|
||||
|
||||
/// Creates default market for tests
|
||||
pub fn default_for_tests() -> PerpMarket {
|
||||
PerpMarket {
|
||||
group: Pubkey::new_unique(),
|
||||
perp_market_index: 0,
|
||||
name: Default::default(),
|
||||
oracle: Pubkey::new_unique(),
|
||||
oracle_config: OracleConfig {
|
||||
conf_filter: I80F48::ZERO,
|
||||
},
|
||||
bids: Pubkey::new_unique(),
|
||||
asks: Pubkey::new_unique(),
|
||||
event_queue: Pubkey::new_unique(),
|
||||
quote_lot_size: 1,
|
||||
base_lot_size: 1,
|
||||
maint_asset_weight: I80F48::from(1),
|
||||
init_asset_weight: I80F48::from(1),
|
||||
maint_liab_weight: I80F48::from(1),
|
||||
init_liab_weight: I80F48::from(1),
|
||||
liquidation_fee: I80F48::ZERO,
|
||||
maker_fee: I80F48::ZERO,
|
||||
taker_fee: I80F48::ZERO,
|
||||
min_funding: I80F48::ZERO,
|
||||
max_funding: I80F48::ZERO,
|
||||
impact_quantity: 0,
|
||||
long_funding: I80F48::ZERO,
|
||||
short_funding: I80F48::ZERO,
|
||||
funding_last_updated: 0,
|
||||
open_interest: 0,
|
||||
seq_num: 0,
|
||||
fees_accrued: I80F48::ZERO,
|
||||
fees_settled: I80F48::ZERO,
|
||||
bump: 0,
|
||||
base_decimals: 0,
|
||||
reserved: [0; 108],
|
||||
padding0: Default::default(),
|
||||
padding1: Default::default(),
|
||||
padding2: Default::default(),
|
||||
registration_time: 0,
|
||||
fee_penalty: 0.0,
|
||||
trusted_market: 0,
|
||||
group_insurance_fund: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2143,6 +2143,7 @@ pub struct PerpCreateMarketInstruction {
|
|||
pub taker_fee: f32,
|
||||
pub group_insurance_fund: bool,
|
||||
pub trusted_market: bool,
|
||||
pub fee_penalty: f32,
|
||||
}
|
||||
impl PerpCreateMarketInstruction {
|
||||
pub async fn with_new_book_and_queue(
|
||||
|
@ -2195,6 +2196,7 @@ impl ClientInstruction for PerpCreateMarketInstruction {
|
|||
base_decimals: self.base_decimals,
|
||||
group_insurance_fund: self.group_insurance_fund,
|
||||
trusted_market: self.trusted_market,
|
||||
fee_penalty: self.fee_penalty,
|
||||
};
|
||||
|
||||
let perp_market = Pubkey::find_program_address(
|
||||
|
|
|
@ -1330,6 +1330,7 @@ export class MangoClient {
|
|||
liquidationFee: number,
|
||||
makerFee: number,
|
||||
takerFee: number,
|
||||
feePenalty: number,
|
||||
minFunding: number,
|
||||
maxFunding: number,
|
||||
impactQuantity: number,
|
||||
|
@ -1364,6 +1365,7 @@ export class MangoClient {
|
|||
new BN(impactQuantity),
|
||||
groupInsuranceFund,
|
||||
trustedMarket,
|
||||
feePenalty
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
|
@ -1430,6 +1432,7 @@ export class MangoClient {
|
|||
liquidationFee: number,
|
||||
makerFee: number,
|
||||
takerFee: number,
|
||||
feePenalty: number,
|
||||
minFunding: number,
|
||||
maxFunding: number,
|
||||
impactQuantity: number,
|
||||
|
@ -1459,6 +1462,7 @@ export class MangoClient {
|
|||
new BN(impactQuantity),
|
||||
groupInsuranceFund,
|
||||
trustedMarket,
|
||||
feePenalty
|
||||
)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
|
|
|
@ -2315,6 +2315,10 @@ export type MangoV4 = {
|
|||
{
|
||||
"name": "trustedMarket",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "feePenalty",
|
||||
"type": "f32"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -2429,6 +2433,12 @@ export type MangoV4 = {
|
|||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feePenaltyOpt",
|
||||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -4014,12 +4024,16 @@ export type MangoV4 = {
|
|||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feePenalty",
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
112
|
||||
108
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -8402,6 +8416,10 @@ export const IDL: MangoV4 = {
|
|||
{
|
||||
"name": "trustedMarket",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "feePenalty",
|
||||
"type": "f32"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -8516,6 +8534,12 @@ export const IDL: MangoV4 = {
|
|||
"type": {
|
||||
"option": "bool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feePenaltyOpt",
|
||||
"type": {
|
||||
"option": "f32"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -10101,12 +10125,16 @@ export const IDL: MangoV4 = {
|
|||
"defined": "I80F48"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feePenalty",
|
||||
"type": "f32"
|
||||
},
|
||||
{
|
||||
"name": "reserved",
|
||||
"type": {
|
||||
"array": [
|
||||
"u8",
|
||||
112
|
||||
108
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue