diff --git a/DEVELOPING.md b/DEVELOPING.md index d6a75f63b..8b6e43f69 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -10,3 +10,9 @@ _work in progress_ ### Code style ### Testing + +In order to run the tests the `enable_gpl` feature needs to be enabled to not skip essential tests. + +``` +cargo test-sbf --features enable-gpl +``` \ No newline at end of file diff --git a/bin/liquidator/src/rebalance.rs b/bin/liquidator/src/rebalance.rs index 6b8bf33e2..89b7a1149 100644 --- a/bin/liquidator/src/rebalance.rs +++ b/bin/liquidator/src/rebalance.rs @@ -335,6 +335,7 @@ impl Rebalancer { true, // reduce only 0, 10, + mango_v4::state::SelfTradeBehavior::DecrementTake, ) .await?; log::info!( diff --git a/lib/client/src/client.rs b/lib/client/src/client.rs index 68e0657fc..42e335abb 100644 --- a/lib/client/src/client.rs +++ b/lib/client/src/client.rs @@ -17,8 +17,8 @@ use itertools::Itertools; use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; use mango_v4::state::{ - Bank, Group, MangoAccountValue, PerpMarketIndex, PlaceOrderType, Serum3MarketIndex, Side, - TokenIndex, INSURANCE_TOKEN_INDEX, + Bank, Group, MangoAccountValue, PerpMarketIndex, PlaceOrderType, SelfTradeBehavior, + Serum3MarketIndex, Side, TokenIndex, INSURANCE_TOKEN_INDEX, }; use solana_address_lookup_table_program::state::AddressLookupTable; @@ -795,6 +795,7 @@ impl MangoClient { reduce_only: bool, expiry_timestamp: u64, limit: u8, + self_trade_behavior: SelfTradeBehavior, ) -> anyhow::Result { let perp = self.context.perp(market_index); let health_remaining_metas = self.context.derive_health_check_remaining_account_metas( @@ -823,7 +824,7 @@ impl MangoClient { ams.extend(health_remaining_metas.into_iter()); ams }, - data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpPlaceOrder { + data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpPlaceOrderV2 { side, price_lots, max_base_lots, @@ -833,6 +834,7 @@ impl MangoClient { reduce_only, expiry_timestamp, limit, + self_trade_behavior, }), }; @@ -851,6 +853,7 @@ impl MangoClient { reduce_only: bool, expiry_timestamp: u64, limit: u8, + self_trade_behavior: SelfTradeBehavior, ) -> anyhow::Result { let account = self.mango_account().await?; let ix = self.perp_place_order_instruction( @@ -865,6 +868,7 @@ impl MangoClient { reduce_only, expiry_timestamp, limit, + self_trade_behavior, )?; self.send_and_confirm_owner_tx(vec![ix]).await } diff --git a/mango_v4.json b/mango_v4.json index f6dfc2d02..a45b80e4c 100644 --- a/mango_v4.json +++ b/mango_v4.json @@ -3593,6 +3593,112 @@ "option": "u128" } }, + { + "name": "perpPlaceOrderV2", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false, + "relations": [ + "group", + "bids", + "asks", + "event_queue", + "oracle" + ] + }, + { + "name": "bids", + "isMut": true, + "isSigner": false + }, + { + "name": "asks", + "isMut": true, + "isSigner": false + }, + { + "name": "eventQueue", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "side", + "type": { + "defined": "Side" + } + }, + { + "name": "priceLots", + "type": "i64" + }, + { + "name": "maxBaseLots", + "type": "i64" + }, + { + "name": "maxQuoteLots", + "type": "i64" + }, + { + "name": "clientOrderId", + "type": "u64" + }, + { + "name": "orderType", + "type": { + "defined": "PlaceOrderType" + } + }, + { + "name": "selfTradeBehavior", + "type": { + "defined": "SelfTradeBehavior" + } + }, + { + "name": "reduceOnly", + "type": "bool" + }, + { + "name": "expiryTimestamp", + "type": "u64" + }, + { + "name": "limit", + "type": "u8" + } + ], + "returns": { + "option": "u128" + } + }, { "name": "perpPlaceOrderPegged", "accounts": [ @@ -3701,6 +3807,120 @@ "option": "u128" } }, + { + "name": "perpPlaceOrderPeggedV2", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false, + "relations": [ + "group", + "bids", + "asks", + "event_queue", + "oracle" + ] + }, + { + "name": "bids", + "isMut": true, + "isSigner": false + }, + { + "name": "asks", + "isMut": true, + "isSigner": false + }, + { + "name": "eventQueue", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "side", + "type": { + "defined": "Side" + } + }, + { + "name": "priceOffsetLots", + "type": "i64" + }, + { + "name": "pegLimit", + "type": "i64" + }, + { + "name": "maxBaseLots", + "type": "i64" + }, + { + "name": "maxQuoteLots", + "type": "i64" + }, + { + "name": "clientOrderId", + "type": "u64" + }, + { + "name": "orderType", + "type": { + "defined": "PlaceOrderType" + } + }, + { + "name": "selfTradeBehavior", + "type": { + "defined": "SelfTradeBehavior" + } + }, + { + "name": "reduceOnly", + "type": "bool" + }, + { + "name": "expiryTimestamp", + "type": "u64" + }, + { + "name": "limit", + "type": "u8" + }, + { + "name": "maxOracleStalenessSlots", + "type": "i32" + } + ], + "returns": { + "option": "u128" + } + }, { "name": "perpCancelOrder", "accounts": [ @@ -7721,6 +7941,28 @@ ] } }, + { + "name": "SelfTradeBehavior", + "docs": [ + "Self trade behavior controls how taker orders interact with resting limit orders of the same account.", + "This setting has no influence on placing a resting or oracle pegged limit order that does not match", + "immediately, instead it's the responsibility of the user to correctly configure his taker orders." + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "DecrementTake" + }, + { + "name": "CancelProvide" + }, + { + "name": "AbortTransaction" + } + ] + } + }, { "name": "Side", "type": { @@ -9205,6 +9447,11 @@ "type": "i64", "index": false }, + { + "name": "totalQuoteLotsDecremented", + "type": "i64", + "index": false + }, { "name": "takerFeesPaid", "type": "i128", @@ -9553,6 +9800,11 @@ "code": 6047, "name": "InvalidHealthAccountCount", "msg": "incorrect number of health accounts" + }, + { + "code": 6048, + "name": "WouldSelfTrade", + "msg": "would self trade" } ] } \ No newline at end of file diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index 52232e17f..901a5117b 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -101,6 +101,8 @@ pub enum MangoError { TokenInForceClose, #[msg("incorrect number of health accounts")] InvalidHealthAccountCount, + #[msg("would self trade")] + WouldSelfTrade, } impl MangoError { diff --git a/programs/mango-v4/src/instructions/perp_place_order.rs b/programs/mango-v4/src/instructions/perp_place_order.rs index fe7199b1e..b88da6099 100644 --- a/programs/mango-v4/src/instructions/perp_place_order.rs +++ b/programs/mango-v4/src/instructions/perp_place_order.rs @@ -210,7 +210,8 @@ mod tests { client_order_id: 0, reduce_only: true, time_in_force: 0, - params: OrderParams::Market, + self_trade_behavior: SelfTradeBehavior::DecrementTake, + params: OrderParams::Market {}, }; let result = reduce_only_max_base_lots(&pp, &order, market_reduce_only); diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 745fce97c..ffdffb4df 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -31,13 +31,15 @@ pub mod instructions; compile_error!("compiling the program entrypoint without 'enable-gpl' makes no sense, enable it or use the 'cpi' or 'client' features"); use state::{ - OracleConfigParams, PerpMarketIndex, PlaceOrderType, Serum3MarketIndex, Side, TokenIndex, + OracleConfigParams, PerpMarketIndex, PlaceOrderType, SelfTradeBehavior, Serum3MarketIndex, + Side, TokenIndex, }; declare_id!("4MangoMjqJ2firMokCjjGgoK8d4MXcrgL7XJaL3w6fVg"); #[program] pub mod mango_v4 { + use super::*; use error::*; @@ -785,8 +787,74 @@ pub mod mango_v4 { client_order_id, reduce_only, time_in_force, + self_trade_behavior: SelfTradeBehavior::default(), params: match order_type { - PlaceOrderType::Market => OrderParams::Market, + PlaceOrderType::Market => OrderParams::Market {}, + PlaceOrderType::ImmediateOrCancel => OrderParams::ImmediateOrCancel { price_lots }, + _ => OrderParams::Fixed { + price_lots, + order_type: order_type.to_post_order_type()?, + }, + }, + }; + #[cfg(feature = "enable-gpl")] + return instructions::perp_place_order(ctx, order, limit); + + #[cfg(not(feature = "enable-gpl"))] + Ok(None) + } + + #[allow(clippy::too_many_arguments)] + pub fn perp_place_order_v2( + ctx: Context, + side: Side, + + // The price in lots (quote lots per base lots) + // - fill orders on the book up to this price or + // - place an order on the book at this price. + // - ignored for Market orders and potentially adjusted for PostOnlySlide orders. + price_lots: i64, + + max_base_lots: i64, + max_quote_lots: i64, + client_order_id: u64, + order_type: PlaceOrderType, + self_trade_behavior: SelfTradeBehavior, + reduce_only: bool, + + // Timestamp of when order expires + // + // Send 0 if you want the order to never expire. + // Timestamps in the past mean the instruction is skipped. + // Timestamps in the future are reduced to now + 65535s. + expiry_timestamp: u64, + + // Maximum number of orders from the book to fill. + // + // Use this to limit compute used during order matching. + // When the limit is reached, processing stops and the instruction succeeds. + limit: u8, + ) -> Result> { + require_gte!(price_lots, 0); + + use crate::state::{Order, OrderParams}; + let time_in_force = match Order::tif_from_expiry(expiry_timestamp) { + Some(t) => t, + None => { + msg!("Order is already expired"); + return Ok(None); + } + }; + let order = Order { + side, + max_base_lots, + max_quote_lots, + client_order_id, + reduce_only, + time_in_force, + self_trade_behavior, + params: match order_type { + PlaceOrderType::Market => OrderParams::Market {}, PlaceOrderType::ImmediateOrCancel => OrderParams::ImmediateOrCancel { price_lots }, _ => OrderParams::Fixed { price_lots, @@ -858,6 +926,80 @@ pub mod mango_v4 { client_order_id, reduce_only, time_in_force, + self_trade_behavior: SelfTradeBehavior::DecrementTake, + params: OrderParams::OraclePegged { + price_offset_lots, + order_type: order_type.to_post_order_type()?, + peg_limit, + max_oracle_staleness_slots, + }, + }; + #[cfg(feature = "enable-gpl")] + return instructions::perp_place_order(ctx, order, limit); + + #[cfg(not(feature = "enable-gpl"))] + Ok(None) + } + + #[allow(clippy::too_many_arguments)] + pub fn perp_place_order_pegged_v2( + ctx: Context, + side: Side, + + // The adjustment from the oracle price, in lots (quote lots per base lots). + // Orders on the book may be filled at oracle + adjustment (depends on order type). + price_offset_lots: i64, + + // The limit at which the pegged order shall expire. + // May be -1 to denote no peg limit. + // + // Example: An bid pegged to -20 with peg_limit 100 would expire if the oracle hits 121. + peg_limit: i64, + + max_base_lots: i64, + max_quote_lots: i64, + client_order_id: u64, + order_type: PlaceOrderType, + self_trade_behavior: SelfTradeBehavior, + reduce_only: bool, + + // Timestamp of when order expires + // + // Send 0 if you want the order to never expire. + // Timestamps in the past mean the instruction is skipped. + // Timestamps in the future are reduced to now + 65535s. + expiry_timestamp: u64, + + // Maximum number of orders from the book to fill. + // + // Use this to limit compute used during order matching. + // When the limit is reached, processing stops and the instruction succeeds. + limit: u8, + + // Oracle staleness limit, in slots. Set to -1 to disable. + // + // WARNING: Not currently implemented. + max_oracle_staleness_slots: i32, + ) -> Result> { + require_gte!(peg_limit, -1); + require_eq!(max_oracle_staleness_slots, -1); // unimplemented + + use crate::state::{Order, OrderParams}; + let time_in_force = match Order::tif_from_expiry(expiry_timestamp) { + Some(t) => t, + None => { + msg!("Order is already expired"); + return Ok(None); + } + }; + let order = Order { + side, + max_base_lots, + max_quote_lots, + client_order_id, + reduce_only, + time_in_force, + self_trade_behavior, params: OrderParams::OraclePegged { price_offset_lots, order_type: order_type.to_post_order_type()?, diff --git a/programs/mango-v4/src/logs.rs b/programs/mango-v4/src/logs.rs index 0fe28c111..9dda8d543 100644 --- a/programs/mango-v4/src/logs.rs +++ b/programs/mango-v4/src/logs.rs @@ -404,9 +404,10 @@ pub struct PerpTakerTradeLog { pub perp_market_index: u16, pub taker_side: u8, pub total_base_lots_taken: i64, - pub total_quote_lots_taken: i64, // exclusive fees paid - pub taker_fees_paid: i128, // in native quote units - pub fee_penalty: i128, // in native quote units + pub total_quote_lots_taken: i64, // exclusive fees paid + pub total_quote_lots_decremented: i64, // from DecrementTake self-trades + pub taker_fees_paid: i128, // in native quote units + pub fee_penalty: i128, // in native quote units } #[event] diff --git a/programs/mango-v4/src/state/orderbook/book.rs b/programs/mango-v4/src/state/orderbook/book.rs index 3b33d4fb2..5ef70af39 100644 --- a/programs/mango-v4/src/state/orderbook/book.rs +++ b/programs/mango-v4/src/state/orderbook/book.rs @@ -78,8 +78,9 @@ impl<'a> Orderbook<'a> { // matched_changes/matched_deletes and then applied after this loop. let mut remaining_base_lots = order.max_base_lots; let mut remaining_quote_lots = order.max_quote_lots; - let mut matched_order_changes: Vec<(BookSideOrderHandle, i64)> = vec![]; - let mut matched_order_deletes: Vec<(BookSideOrderTree, u128)> = vec![]; + let mut decremented_quote_lots = 0i64; + let mut orders_to_change: Vec<(BookSideOrderHandle, i64)> = vec![]; + let mut orders_to_delete: Vec<(BookSideOrderTree, u128)> = vec![]; let mut number_of_dropped_expired_orders = 0; let opposing_bookside = self.bookside_mut(other_side); for best_opposing in opposing_bookside.iter_all_including_invalid(now_ts, oracle_price_lots) @@ -101,7 +102,7 @@ impl<'a> Orderbook<'a> { best_opposing.node.quantity, ); event_queue.push_back(cast(event)).unwrap(); - matched_order_deletes + orders_to_delete .push((best_opposing.handle.order_tree, best_opposing.node.key)); } continue; @@ -129,8 +130,36 @@ impl<'a> Orderbook<'a> { let match_base_lots = remaining_base_lots .min(best_opposing.node.quantity) .min(max_match_by_quote); - let match_quote_lots = match_base_lots * best_opposing_price; + + let order_would_self_trade = *mango_account_pk == best_opposing.node.owner; + if order_would_self_trade { + match order.self_trade_behavior { + SelfTradeBehavior::DecrementTake => { + // remember all decremented quote lots to only charge fees on not-self-trades + decremented_quote_lots += match_quote_lots; + } + SelfTradeBehavior::CancelProvide => { + let event = OutEvent::new( + other_side, + best_opposing.node.owner_slot, + now_ts, + event_queue.header.seq_num, + best_opposing.node.owner, + best_opposing.node.quantity, + ); + event_queue.push_back(cast(event)).unwrap(); + orders_to_delete + .push((best_opposing.handle.order_tree, best_opposing.node.key)); + + // skip actual matching + continue; + } + SelfTradeBehavior::AbortTransaction => return err!(MangoError::WouldSelfTrade), + } + assert!(order.self_trade_behavior == SelfTradeBehavior::DecrementTake); + } + remaining_base_lots -= match_base_lots; remaining_quote_lots -= match_quote_lots; assert!(remaining_quote_lots >= 0); @@ -138,12 +167,12 @@ impl<'a> Orderbook<'a> { let new_best_opposing_quantity = best_opposing.node.quantity - match_base_lots; let maker_out = new_best_opposing_quantity == 0; if maker_out { - matched_order_deletes - .push((best_opposing.handle.order_tree, best_opposing.node.key)); + orders_to_delete.push((best_opposing.handle.order_tree, best_opposing.node.key)); } else { - matched_order_changes.push((best_opposing.handle, new_best_opposing_quantity)); + orders_to_change.push((best_opposing.handle, new_best_opposing_quantity)); } + // order_would_self_trade is only true in the DecrementTake case, in which we don't charge fees let seq_num = event_queue.header.seq_num; let fill = FillEvent::new( side, @@ -153,11 +182,20 @@ impl<'a> Orderbook<'a> { seq_num, best_opposing.node.owner, best_opposing.node.client_order_id, - market.maker_fee, + if order_would_self_trade { + I80F48::ZERO + } else { + market.maker_fee + }, best_opposing.node.timestamp, *mango_account_pk, order.client_order_id, - market.taker_fee, + if order_would_self_trade { + I80F48::ZERO + } else { + // NOTE: this does not include the IOC penalty, but this value is not used to calculate fees + market.taker_fee + }, best_opposing_price, match_base_lots, ); @@ -179,7 +217,12 @@ impl<'a> Orderbook<'a> { // realized when the fill event gets executed if total_quote_lots_taken > 0 || total_base_lots_taken > 0 { perp_position.add_taker_trade(side, total_base_lots_taken, total_quote_lots_taken); - let taker_fees_paid = apply_fees(market, mango_account, total_quote_lots_taken)?; + // reduce fees to apply by decrement take volume + let taker_fees_paid = apply_fees( + market, + mango_account, + total_quote_lots_taken - decremented_quote_lots, + )?; emit!(PerpTakerTradeLog { mango_group: market.group.key(), mango_account: *mango_account_pk, @@ -187,13 +230,14 @@ impl<'a> Orderbook<'a> { taker_side: side as u8, total_base_lots_taken, total_quote_lots_taken, + total_quote_lots_decremented: decremented_quote_lots, taker_fees_paid: taker_fees_paid.to_bits(), fee_penalty: fee_penalty.to_bits(), }); } // Apply changes to matched asks (handles invalidate on delete!) - for (handle, new_quantity) in matched_order_changes { + for (handle, new_quantity) in orders_to_change { opposing_bookside .node_mut(handle.node) .unwrap() @@ -201,7 +245,7 @@ impl<'a> Orderbook<'a> { .unwrap() .quantity = new_quantity; } - for (component, key) in matched_order_deletes { + for (component, key) in orders_to_delete { let _removed_leaf = opposing_bookside.remove_by_key(component, key).unwrap(); } @@ -396,6 +440,8 @@ fn apply_fees( let perp_position = account.perp_position_mut(market.perp_market_index)?; perp_position.record_trading_fee(taker_fees); + + // taker fees are applied to volume during matching, quote volume only during consume perp_position.taker_volume += taker_fees.to_num::(); // Accrue maker fees immediately: they can be negative and applying them later diff --git a/programs/mango-v4/src/state/orderbook/mod.rs b/programs/mango-v4/src/state/orderbook/mod.rs index a2e23b5e1..ea4c6fb0e 100644 --- a/programs/mango-v4/src/state/orderbook/mod.rs +++ b/programs/mango-v4/src/state/orderbook/mod.rs @@ -126,6 +126,7 @@ mod tests { client_order_id: 0, time_in_force, reduce_only: false, + self_trade_behavior: SelfTradeBehavior::DecrementTake, params: OrderParams::Fixed { price_lots, order_type: PostOrderType::Limit, @@ -274,6 +275,7 @@ mod tests { client_order_id: 42, time_in_force: 0, reduce_only: false, + self_trade_behavior: SelfTradeBehavior::DecrementTake, params: OrderParams::Fixed { price_lots, order_type: PostOrderType::Limit, @@ -335,6 +337,7 @@ mod tests { client_order_id: 43, time_in_force: 0, reduce_only: false, + self_trade_behavior: SelfTradeBehavior::DecrementTake, params: OrderParams::Fixed { price_lots, order_type: PostOrderType::Limit, @@ -433,27 +436,36 @@ mod tests { #[test] fn test_fee_penalty_applied_only_on_limit_order() -> Result<()> { + // setup market let (mut market, oracle_price, mut event_queue, book_accs) = test_setup(1000.0); let mut book = book_accs.orderbook(); - - 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.taker_fee = I80F48::from_num(0.01); + market.maker_fee = I80F48::from_num(0.0); market.fee_penalty = 5.0; - account.ensure_perp_position(market.perp_market_index, 0)?; - // Passive order + // setup maker account + let maker_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); + let mut maker_account = MangoAccountValue::from_bytes(&maker_buffer).unwrap(); + let maker_pk = Pubkey::new_unique(); + maker_account.ensure_perp_position(market.perp_market_index, 0)?; + + // setup taker account + let taker_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); + let mut taker_account = MangoAccountValue::from_bytes(&taker_buffer).unwrap(); + let taker_pk = Pubkey::new_unique(); + taker_account.ensure_perp_position(market.perp_market_index, 0)?; + + // Maker order book.new_order( Order { side: Side::Ask, max_base_lots: 2, max_quote_lots: i64::MAX, - client_order_id: 43, + client_order_id: 42, time_in_force: 0, reduce_only: false, + self_trade_behavior: SelfTradeBehavior::default(), params: OrderParams::Fixed { price_lots: 1000, order_type: PostOrderType::Limit, @@ -462,8 +474,8 @@ mod tests { &mut market, &mut event_queue, oracle_price, - &mut account.borrow_mut(), - &taker_pk, + &mut maker_account.borrow_mut(), + &maker_pk, now_ts, u8::MAX, ) @@ -478,6 +490,7 @@ mod tests { client_order_id: 43, time_in_force: 0, reduce_only: false, + self_trade_behavior: SelfTradeBehavior::DecrementTake, params: OrderParams::Fixed { price_lots: 1000, order_type: PostOrderType::Limit, @@ -486,14 +499,14 @@ mod tests { &mut market, &mut event_queue, oracle_price, - &mut account.borrow_mut(), + &mut taker_account.borrow_mut(), &taker_pk, now_ts, u8::MAX, ) .unwrap(); - let pos = account.perp_position(market.perp_market_index)?; + let pos = taker_account.perp_position(market.perp_market_index)?; assert_eq!( pos.quote_position_native().round(), @@ -513,27 +526,28 @@ mod tests { side: Side::Bid, max_base_lots: 1, max_quote_lots: i64::MAX, - client_order_id: 43, + client_order_id: 44, time_in_force: 0, reduce_only: false, + self_trade_behavior: SelfTradeBehavior::DecrementTake, params: OrderParams::ImmediateOrCancel { price_lots: 1000 }, }, &mut market, &mut event_queue, oracle_price, - &mut account.borrow_mut(), + &mut taker_account.borrow_mut(), &taker_pk, now_ts, u8::MAX, ) .unwrap(); - let pos = account.perp_position(market.perp_market_index)?; + let pos = taker_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" + "No fees, but fixed penalty applied on IOC order" ); assert_eq!( @@ -574,6 +588,7 @@ mod tests { client_order_id: 0, time_in_force: 0, reduce_only: false, + self_trade_behavior: SelfTradeBehavior::DecrementTake, params: OrderParams::Fixed { price_lots, order_type: PostOrderType::Limit, @@ -606,4 +621,368 @@ mod tests { new_order(&mut book, &mut event_queue, Side::Bid, 5005, 30, 1); assert_eq!(event_queue.len(), 1); } + + #[test] + fn test_self_trade_decrement_take() -> Result<()> { + // setup market + let (mut market, oracle_price, mut event_queue, book_accs) = test_setup(1000.0); + let mut book = book_accs.orderbook(); + let now_ts = 1000000; + market.taker_fee = I80F48::from_num(0.01); + market.maker_fee = I80F48::from_num(0.0); + market.fee_penalty = 5.0; + + // setup maker account + let maker_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); + let mut maker_account = MangoAccountValue::from_bytes(&maker_buffer).unwrap(); + let maker_pk = Pubkey::new_unique(); + maker_account.ensure_perp_position(market.perp_market_index, 0)?; + + // setup taker account + let taker_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); + let mut taker_account = MangoAccountValue::from_bytes(&taker_buffer).unwrap(); + let taker_pk = Pubkey::new_unique(); + taker_account.ensure_perp_position(market.perp_market_index, 0)?; + + // taker limit order + book.new_order( + Order { + side: Side::Ask, + max_base_lots: 2, + max_quote_lots: i64::MAX, + client_order_id: 1, + time_in_force: 0, + reduce_only: false, + self_trade_behavior: SelfTradeBehavior::default(), + params: OrderParams::Fixed { + price_lots: 1000, + order_type: PostOrderType::Limit, + }, + }, + &mut market, + &mut event_queue, + oracle_price, + &mut taker_account.borrow_mut(), + &taker_pk, + now_ts, + u8::MAX, + ) + .unwrap(); + + // maker limit order + book.new_order( + Order { + side: Side::Ask, + max_base_lots: 2, + max_quote_lots: i64::MAX, + client_order_id: 2, + time_in_force: 0, + reduce_only: false, + self_trade_behavior: SelfTradeBehavior::default(), + params: OrderParams::Fixed { + price_lots: 1000, + order_type: PostOrderType::Limit, + }, + }, + &mut market, + &mut event_queue, + oracle_price, + &mut maker_account.borrow_mut(), + &maker_pk, + now_ts, + u8::MAX, + ) + .unwrap(); + + // taker full self-trade IOC + book.new_order( + Order { + side: Side::Bid, + max_base_lots: 1, + max_quote_lots: i64::MAX, + client_order_id: 3, + time_in_force: 0, + reduce_only: false, + self_trade_behavior: SelfTradeBehavior::DecrementTake, + params: OrderParams::ImmediateOrCancel { price_lots: 1000 }, + }, + &mut market, + &mut event_queue, + oracle_price, + &mut taker_account.borrow_mut(), + &taker_pk, + now_ts, + u8::MAX, + ) + .unwrap(); + + let pos = taker_account.perp_position(market.perp_market_index)?; + + assert_eq!( + pos.quote_position_native().round(), + I80F48::from_num(-5), + "Penalty applied on ioc self-trade" + ); + + assert_eq!( + market.fees_accrued.round(), + I80F48::from_num(5), + "Fees moved to market" + ); + + let fill_event: FillEvent = event_queue.pop_front()?.try_into()?; + assert_eq!(fill_event.quantity, 1); + assert_eq!(fill_event.maker, taker_pk); + assert_eq!(fill_event.taker, taker_pk); + assert_eq!(fill_event.maker_fee, I80F48::ZERO); + assert_eq!(fill_event.taker_fee, I80F48::ZERO); + + // taker partial self trade limit + book.new_order( + Order { + side: Side::Bid, + max_base_lots: 2, + max_quote_lots: i64::MAX, + client_order_id: 4, + time_in_force: 0, + reduce_only: false, + self_trade_behavior: SelfTradeBehavior::DecrementTake, + params: OrderParams::Fixed { + price_lots: 1000, + order_type: PostOrderType::Limit, + }, + }, + &mut market, + &mut event_queue, + oracle_price, + &mut taker_account.borrow_mut(), + &taker_pk, + now_ts, + u8::MAX, + ) + .unwrap(); + + let pos = taker_account.perp_position(market.perp_market_index)?; + + assert_eq!( + pos.quote_position_native().round(), + I80F48::from_num(-15), // -0 -10 + "No fees for self-trade but for maker match" + ); + + assert_eq!( + market.fees_accrued.round(), + I80F48::from_num(15), // 0 +10 + "Fees moved to market" + ); + + let fill_event: FillEvent = event_queue.pop_front()?.try_into()?; + assert_eq!(fill_event.quantity, 1); + assert_eq!(fill_event.maker, taker_pk); + assert_eq!(fill_event.taker, taker_pk); + assert_eq!(fill_event.maker_fee, I80F48::ZERO); + assert_eq!(fill_event.taker_fee, I80F48::ZERO); + + let fill_event: FillEvent = event_queue.pop_front()?.try_into()?; + assert_eq!(fill_event.quantity, 1); + assert_eq!(fill_event.maker, maker_pk); + assert_eq!(fill_event.taker, taker_pk); + assert_eq!(fill_event.maker_fee, 0.0); + assert_eq!(fill_event.taker_fee, 0.01); + + Ok(()) + } + + #[test] + fn test_self_trade_cancel_provide() -> Result<()> { + // setup market + let (mut market, oracle_price, mut event_queue, book_accs) = test_setup(1000.0); + let mut book = book_accs.orderbook(); + let now_ts = 1000000; + market.taker_fee = I80F48::from_num(0.01); + market.maker_fee = I80F48::from_num(0.0); + market.fee_penalty = 5.0; + + // setup maker account + let maker_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); + let mut maker_account = MangoAccountValue::from_bytes(&maker_buffer).unwrap(); + let maker_pk = Pubkey::new_unique(); + maker_account.ensure_perp_position(market.perp_market_index, 0)?; + + // setup taker account + let taker_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); + let mut taker_account = MangoAccountValue::from_bytes(&taker_buffer).unwrap(); + let taker_pk = Pubkey::new_unique(); + taker_account.ensure_perp_position(market.perp_market_index, 0)?; + + // taker limit order + book.new_order( + Order { + side: Side::Ask, + max_base_lots: 1, + max_quote_lots: i64::MAX, + client_order_id: 1, + time_in_force: 0, + reduce_only: false, + self_trade_behavior: SelfTradeBehavior::default(), + params: OrderParams::Fixed { + price_lots: 1000, + order_type: PostOrderType::Limit, + }, + }, + &mut market, + &mut event_queue, + oracle_price, + &mut taker_account.borrow_mut(), + &taker_pk, + now_ts, + u8::MAX, + ) + .unwrap(); + + // maker limit order + book.new_order( + Order { + side: Side::Ask, + max_base_lots: 2, + max_quote_lots: i64::MAX, + client_order_id: 2, + time_in_force: 0, + reduce_only: false, + self_trade_behavior: SelfTradeBehavior::default(), + params: OrderParams::Fixed { + price_lots: 1000, + order_type: PostOrderType::Limit, + }, + }, + &mut market, + &mut event_queue, + oracle_price, + &mut maker_account.borrow_mut(), + &maker_pk, + now_ts, + u8::MAX, + ) + .unwrap(); + + // taker partial self-trade + book.new_order( + Order { + side: Side::Bid, + max_base_lots: 1, + max_quote_lots: i64::MAX, + client_order_id: 3, + time_in_force: 0, + reduce_only: false, + self_trade_behavior: SelfTradeBehavior::CancelProvide, + params: OrderParams::Fixed { + price_lots: 1000, + order_type: PostOrderType::Limit, + }, + }, + &mut market, + &mut event_queue, + oracle_price, + &mut taker_account.borrow_mut(), + &taker_pk, + now_ts, + u8::MAX, + ) + .unwrap(); + + let pos = taker_account.perp_position(market.perp_market_index)?; + assert_eq!( + pos.quote_position_native().round(), + I80F48::from_num(-10), // -0 -10 + "No fees for self-trade but for maker match" + ); + + assert_eq!( + market.fees_accrued.round(), + I80F48::from_num(10), // 0 +10 + "Fees moved to market" + ); + + let out_event: OutEvent = event_queue.pop_front()?.try_into()?; + assert_eq!(out_event.owner, taker_pk); + + let fill_event: FillEvent = event_queue.pop_front()?.try_into()?; + assert_eq!(fill_event.maker, maker_pk); + assert_eq!(fill_event.taker, taker_pk); + assert_eq!(fill_event.quantity, 1); + + Ok(()) + } + + #[test] + fn test_self_trade_abort_transaction() -> Result<()> { + // setup market + let (mut market, oracle_price, mut event_queue, book_accs) = test_setup(1000.0); + let mut book = book_accs.orderbook(); + let now_ts = 1000000; + market.taker_fee = I80F48::from_num(0.01); + market.maker_fee = I80F48::from_num(0.0); + market.fee_penalty = 5.0; + + // setup maker account + let maker_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); + let mut maker_account = MangoAccountValue::from_bytes(&maker_buffer).unwrap(); + let maker_pk = Pubkey::new_unique(); + maker_account.ensure_perp_position(market.perp_market_index, 0)?; + + // setup taker account + let taker_buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); + let mut taker_account = MangoAccountValue::from_bytes(&taker_buffer).unwrap(); + let taker_pk = Pubkey::new_unique(); + taker_account.ensure_perp_position(market.perp_market_index, 0)?; + + // taker limit order + book.new_order( + Order { + side: Side::Ask, + max_base_lots: 1, + max_quote_lots: i64::MAX, + client_order_id: 1, + time_in_force: 0, + reduce_only: false, + self_trade_behavior: SelfTradeBehavior::default(), + params: OrderParams::Fixed { + price_lots: 1000, + order_type: PostOrderType::Limit, + }, + }, + &mut market, + &mut event_queue, + oracle_price, + &mut taker_account.borrow_mut(), + &taker_pk, + now_ts, + u8::MAX, + ) + .unwrap(); + + // taker failing self-trade + book.new_order( + Order { + side: Side::Bid, + max_base_lots: 1, + max_quote_lots: i64::MAX, + client_order_id: 3, + time_in_force: 0, + reduce_only: false, + self_trade_behavior: SelfTradeBehavior::AbortTransaction, + params: OrderParams::ImmediateOrCancel { price_lots: 1000 }, + }, + &mut market, + &mut event_queue, + oracle_price, + &mut taker_account.borrow_mut(), + &taker_pk, + now_ts, + u8::MAX, + ) + .expect_err("should fail"); + + Ok(()) + } } diff --git a/programs/mango-v4/src/state/orderbook/order.rs b/programs/mango-v4/src/state/orderbook/order.rs index 6d64c84ea..902989bcf 100644 --- a/programs/mango-v4/src/state/orderbook/order.rs +++ b/programs/mango-v4/src/state/orderbook/order.rs @@ -21,6 +21,9 @@ pub struct Order { /// Number of seconds the order shall live, 0 meaning forever pub time_in_force: u16, + /// Configure how matches with order of the same owner are handled + pub self_trade_behavior: SelfTradeBehavior, + /// Order type specific params pub params: OrderParams, } @@ -120,8 +123,8 @@ impl Order { order_book: &Orderbook, ) -> Result<(i64, u64)> { let price_lots = match self.params { - OrderParams::Market => market_order_limit_for_side(self.side), - OrderParams::ImmediateOrCancel { price_lots } => price_lots, + OrderParams::Market { .. } => market_order_limit_for_side(self.side), + OrderParams::ImmediateOrCancel { price_lots, .. } => price_lots, OrderParams::Fixed { price_lots, order_type, diff --git a/programs/mango-v4/src/state/orderbook/order_type.rs b/programs/mango-v4/src/state/orderbook/order_type.rs index 7414d52ff..f479eefdb 100644 --- a/programs/mango-v4/src/state/orderbook/order_type.rs +++ b/programs/mango-v4/src/state/orderbook/order_type.rs @@ -79,6 +79,35 @@ pub enum PostOrderType { PostOnlySlide = 4, } +#[derive( + Eq, + PartialEq, + Copy, + Clone, + Default, + TryFromPrimitive, + IntoPrimitive, + Debug, + AnchorSerialize, + AnchorDeserialize, +)] +#[repr(u8)] +/// Self trade behavior controls how taker orders interact with resting limit orders of the same account. +/// This setting has no influence on placing a resting or oracle pegged limit order that does not match +/// immediately, instead it's the responsibility of the user to correctly configure his taker orders. +pub enum SelfTradeBehavior { + /// Both the maker and taker sides of the matched orders are decremented. + /// This is equivalent to a normal order match, except for the fact that no fees are applied. + #[default] + DecrementTake = 0, + + /// Cancels the maker side of the trade, the taker side gets matched with other maker's orders. + CancelProvide = 1, + + /// Cancels the whole transaction as soon as a self-matching scenario is encountered. + AbortTransaction = 2, +} + #[derive( Eq, PartialEq, diff --git a/programs/mango-v4/src/state/orderbook/queue.rs b/programs/mango-v4/src/state/orderbook/queue.rs index 454aa58c3..678793319 100644 --- a/programs/mango-v4/src/state/orderbook/queue.rs +++ b/programs/mango-v4/src/state/orderbook/queue.rs @@ -1,5 +1,6 @@ -use crate::error::MangoError; +use crate::{error::Contextable, error::MangoError, error_msg}; use anchor_lang::prelude::*; +use bytemuck::cast_ref; use fixed::types::I80F48; use num_enum::{IntoPrimitive, TryFromPrimitive}; use static_assertions::const_assert_eq; @@ -263,6 +264,36 @@ impl FillEvent { } } +impl TryFrom for FillEvent { + type Error = error::Error; + + fn try_from(e: AnyEvent) -> Result { + if e.event_type != EventType::Fill as u8 { + Err(error_msg!( + "could not convert event with type={} to FillEvent", + e.event_type + )) + } else { + Ok(*cast_ref(&e)) + } + } +} + +impl<'a> TryFrom<&'a AnyEvent> for &'a FillEvent { + type Error = error::Error; + + fn try_from(e: &'a AnyEvent) -> Result { + if e.event_type != EventType::Fill as u8 { + Err(error_msg!( + "could not convert event with type={} to FillEvent", + e.event_type + )) + } else { + Ok(cast_ref(e)) + } + } +} + #[derive( Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, AnchorSerialize, AnchorDeserialize, )] @@ -307,3 +338,33 @@ impl OutEvent { self.side.try_into().unwrap() } } + +impl TryFrom for OutEvent { + type Error = error::Error; + + fn try_from(e: AnyEvent) -> Result { + if e.event_type != EventType::Out as u8 { + Err(error_msg!( + "could not convert event with type={} to OutEvent", + e.event_type + )) + } else { + Ok(*cast_ref(&e)) + } + } +} + +impl<'a> TryFrom<&'a AnyEvent> for &'a OutEvent { + type Error = error::Error; + + fn try_from(e: &'a AnyEvent) -> Result { + if e.event_type != EventType::Out as u8 { + Err(error_msg!( + "could not convert event with type={} to OutEvent", + e.event_type + )) + } else { + Ok(cast_ref(e)) + } + } +} diff --git a/programs/mango-v4/tests/cases/test_fees_buyback_with_mngo.rs b/programs/mango-v4/tests/cases/test_fees_buyback_with_mngo.rs index 07d91219e..cf86954b9 100644 --- a/programs/mango-v4/tests/cases/test_fees_buyback_with_mngo.rs +++ b/programs/mango-v4/tests/cases/test_fees_buyback_with_mngo.rs @@ -102,9 +102,8 @@ async fn test_fees_buyback_with_mngo() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 20, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 5, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -119,9 +118,8 @@ async fn test_fees_buyback_with_mngo() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 20, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 6, + ..PerpPlaceOrderInstruction::default() }, ) .await diff --git a/programs/mango-v4/tests/cases/test_force_close.rs b/programs/mango-v4/tests/cases/test_force_close.rs index 232e536c5..d9f9ddd11 100644 --- a/programs/mango-v4/tests/cases/test_force_close.rs +++ b/programs/mango-v4/tests/cases/test_force_close.rs @@ -320,9 +320,8 @@ async fn test_force_close_perp() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 5, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -338,9 +337,8 @@ async fn test_force_close_perp() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 6, + ..PerpPlaceOrderInstruction::default() }, ) .await diff --git a/programs/mango-v4/tests/cases/test_health_compute.rs b/programs/mango-v4/tests/cases/test_health_compute.rs index 6f1b04892..df86aa17b 100644 --- a/programs/mango-v4/tests/cases/test_health_compute.rs +++ b/programs/mango-v4/tests/cases/test_health_compute.rs @@ -243,9 +243,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await diff --git a/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs b/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs index 9f79f191f..3e944b755 100644 --- a/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs +++ b/programs/mango-v4/tests/cases/test_liq_perps_bankruptcy.rs @@ -162,9 +162,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - client_order_id: 0, - reduce_only: false, + ..PerpPlaceOrderInstruction::default() }) .await; tx.add_instruction(PerpPlaceOrderInstruction { @@ -174,9 +172,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - client_order_id: 0, - reduce_only: false, + ..PerpPlaceOrderInstruction::default() }) .await; tx.add_instruction(PerpConsumeEventsInstruction { @@ -195,9 +191,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { side: Side::Ask, price_lots: adj_price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - client_order_id: 0, - reduce_only: false, + ..PerpPlaceOrderInstruction::default() }) .await; tx.add_instruction(PerpPlaceOrderInstruction { @@ -207,9 +201,7 @@ async fn test_liq_perps_bankruptcy() -> Result<(), TransportError> { side: Side::Bid, price_lots: adj_price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - client_order_id: 0, - reduce_only: false, + ..PerpPlaceOrderInstruction::default() }) .await; tx.add_instruction(PerpConsumeEventsInstruction { diff --git a/programs/mango-v4/tests/cases/test_liq_perps_base_and_bankruptcy.rs b/programs/mango-v4/tests/cases/test_liq_perps_base_and_bankruptcy.rs index 35478009a..ec98390bc 100644 --- a/programs/mango-v4/tests/cases/test_liq_perps_base_and_bankruptcy.rs +++ b/programs/mango-v4/tests/cases/test_liq_perps_base_and_bankruptcy.rs @@ -138,9 +138,7 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 20, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -154,9 +152,7 @@ async fn test_liq_perps_base_and_bankruptcy() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 20, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await diff --git a/programs/mango-v4/tests/cases/test_liq_perps_force_cancel.rs b/programs/mango-v4/tests/cases/test_liq_perps_force_cancel.rs index 573650322..f30fd2b55 100644 --- a/programs/mango-v4/tests/cases/test_liq_perps_force_cancel.rs +++ b/programs/mango-v4/tests/cases/test_liq_perps_force_cancel.rs @@ -106,9 +106,7 @@ async fn test_liq_perps_force_cancel() -> Result<(), TransportError> { price_lots, // health was 1000 * 0.6 = 600; this order is -14*100*(1.4-1) = -560 max_base_lots: 14, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await diff --git a/programs/mango-v4/tests/cases/test_liq_perps_positive_pnl.rs b/programs/mango-v4/tests/cases/test_liq_perps_positive_pnl.rs index 75e4d09c1..cccd7db6f 100644 --- a/programs/mango-v4/tests/cases/test_liq_perps_positive_pnl.rs +++ b/programs/mango-v4/tests/cases/test_liq_perps_positive_pnl.rs @@ -158,9 +158,7 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 10, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -174,9 +172,7 @@ async fn test_liq_perps_positive_pnl() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 10, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await diff --git a/programs/mango-v4/tests/cases/test_perp.rs b/programs/mango-v4/tests/cases/test_perp.rs index af7e004aa..2ff59916e 100644 --- a/programs/mango-v4/tests/cases/test_perp.rs +++ b/programs/mango-v4/tests/cases/test_perp.rs @@ -96,9 +96,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -138,9 +136,8 @@ async fn test_perp_fixed() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 1, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -173,9 +170,8 @@ async fn test_perp_fixed() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 2, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -207,9 +203,8 @@ async fn test_perp_fixed() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 4, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -241,9 +236,8 @@ async fn test_perp_fixed() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 5, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -259,9 +253,8 @@ async fn test_perp_fixed() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 6, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -333,9 +326,8 @@ async fn test_perp_fixed() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 7, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -351,9 +343,8 @@ async fn test_perp_fixed() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 8, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -616,9 +607,8 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 6, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -699,9 +689,8 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 60, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -729,9 +718,8 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 2, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 61, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -781,9 +769,8 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> { side: Side::Ask, price_lots: price_lots + 2, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 62, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -811,9 +798,8 @@ async fn test_perp_oracle_peg() -> Result<(), TransportError> { side: Side::Ask, price_lots: price_lots + 3, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 63, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -933,9 +919,8 @@ async fn test_perp_realize_partially() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 2, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 5, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -950,9 +935,8 @@ async fn test_perp_realize_partially() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 2, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 6, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -989,9 +973,8 @@ async fn test_perp_realize_partially() -> Result<(), TransportError> { side: Side::Ask, price_lots: perp_market_data.native_price_to_lot(I80F48::from_num(1500)), max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 5, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -1006,9 +989,8 @@ async fn test_perp_realize_partially() -> Result<(), TransportError> { side: Side::Bid, price_lots: perp_market_data.native_price_to_lot(I80F48::from_num(1500)), max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, client_order_id: 6, + ..PerpPlaceOrderInstruction::default() }, ) .await diff --git a/programs/mango-v4/tests/cases/test_perp_settle.rs b/programs/mango-v4/tests/cases/test_perp_settle.rs index 81e27ca69..a835975c6 100644 --- a/programs/mango-v4/tests/cases/test_perp_settle.rs +++ b/programs/mango-v4/tests/cases/test_perp_settle.rs @@ -129,9 +129,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -146,9 +144,7 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -615,9 +611,7 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -632,9 +626,7 @@ async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -884,9 +876,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -901,9 +891,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -1017,9 +1005,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { side: Side::Bid, price_lots: 3 * price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - client_order_id: 0, - reduce_only: false, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -1034,9 +1020,7 @@ async fn test_perp_pnl_settle_limit() -> Result<(), TransportError> { side: Side::Ask, price_lots: 3 * price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - client_order_id: 0, - reduce_only: false, + ..PerpPlaceOrderInstruction::default() }, ) .await diff --git a/programs/mango-v4/tests/cases/test_perp_settle_fees.rs b/programs/mango-v4/tests/cases/test_perp_settle_fees.rs index 66d9de4cc..99d9d7091 100644 --- a/programs/mango-v4/tests/cases/test_perp_settle_fees.rs +++ b/programs/mango-v4/tests/cases/test_perp_settle_fees.rs @@ -208,9 +208,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -225,9 +223,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await diff --git a/programs/mango-v4/tests/cases/test_reduce_only.rs b/programs/mango-v4/tests/cases/test_reduce_only.rs index a5a41b184..99f8b5504 100644 --- a/programs/mango-v4/tests/cases/test_reduce_only.rs +++ b/programs/mango-v4/tests/cases/test_reduce_only.rs @@ -281,9 +281,7 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 2, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -298,9 +296,7 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 2, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -328,9 +324,7 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Bid, price_lots: price_lots / 2, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -347,9 +341,7 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -381,9 +373,7 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await; @@ -399,9 +389,8 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Bid, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, reduce_only: true, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -419,9 +408,7 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -439,9 +426,8 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, reduce_only: true, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -459,9 +445,7 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await; @@ -477,9 +461,8 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, reduce_only: true, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -497,9 +480,7 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await; @@ -515,9 +496,8 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Ask, price_lots, max_base_lots: 1, - max_quote_lots: i64::MAX, reduce_only: true, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -535,9 +515,7 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Bid, price_lots: price_lots / 2, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -555,9 +533,7 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Bid, price_lots: price_lots / 2, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await @@ -575,9 +551,7 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Bid, price_lots: price_lots / 2, max_base_lots: 1, - max_quote_lots: i64::MAX, - reduce_only: false, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await; @@ -593,9 +567,8 @@ async fn test_perp_reduce_only() -> Result<(), TransportError> { side: Side::Bid, price_lots: price_lots / 2, max_base_lots: 1, - max_quote_lots: i64::MAX, reduce_only: true, - client_order_id: 0, + ..PerpPlaceOrderInstruction::default() }, ) .await diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 5eaabbc07..31e6886e5 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -3253,11 +3253,28 @@ pub struct PerpPlaceOrderInstruction { pub max_quote_lots: i64, pub reduce_only: bool, pub client_order_id: u64, + pub self_trade_behavior: SelfTradeBehavior, +} +impl Default for PerpPlaceOrderInstruction { + fn default() -> Self { + Self { + account: Pubkey::default(), + perp_market: Pubkey::default(), + owner: TestKeypair::default(), + side: Side::Bid, + price_lots: 0, + max_base_lots: i64::MAX, + max_quote_lots: i64::MAX, + reduce_only: false, + client_order_id: 0, + self_trade_behavior: SelfTradeBehavior::DecrementTake, + } + } } #[async_trait::async_trait(?Send)] impl ClientInstruction for PerpPlaceOrderInstruction { type Accounts = mango_v4::accounts::PerpPlaceOrder; - type Instruction = mango_v4::instruction::PerpPlaceOrder; + type Instruction = mango_v4::instruction::PerpPlaceOrderV2; async fn to_instruction( &self, account_loader: impl ClientAccountLoader + 'async_trait, @@ -3270,6 +3287,7 @@ impl ClientInstruction for PerpPlaceOrderInstruction { max_quote_lots: self.max_quote_lots, client_order_id: self.client_order_id, order_type: PlaceOrderType::Limit, + self_trade_behavior: self.self_trade_behavior, reduce_only: self.reduce_only, expiry_timestamp: 0, limit: 10, @@ -3324,7 +3342,7 @@ pub struct PerpPlaceOrderPeggedInstruction { #[async_trait::async_trait(?Send)] impl ClientInstruction for PerpPlaceOrderPeggedInstruction { type Accounts = mango_v4::accounts::PerpPlaceOrder; - type Instruction = mango_v4::instruction::PerpPlaceOrderPegged; + type Instruction = mango_v4::instruction::PerpPlaceOrderPeggedV2; async fn to_instruction( &self, account_loader: impl ClientAccountLoader + 'async_trait, @@ -3340,6 +3358,7 @@ impl ClientInstruction for PerpPlaceOrderPeggedInstruction { order_type: PlaceOrderType::Limit, reduce_only: false, expiry_timestamp: 0, + self_trade_behavior: SelfTradeBehavior::DecrementTake, limit: 10, max_oracle_staleness_slots: -1, }; diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 93109d49e..b20b4bd2f 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -3593,6 +3593,112 @@ export type MangoV4 = { "option": "u128" } }, + { + "name": "perpPlaceOrderV2", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false, + "relations": [ + "group", + "bids", + "asks", + "event_queue", + "oracle" + ] + }, + { + "name": "bids", + "isMut": true, + "isSigner": false + }, + { + "name": "asks", + "isMut": true, + "isSigner": false + }, + { + "name": "eventQueue", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "side", + "type": { + "defined": "Side" + } + }, + { + "name": "priceLots", + "type": "i64" + }, + { + "name": "maxBaseLots", + "type": "i64" + }, + { + "name": "maxQuoteLots", + "type": "i64" + }, + { + "name": "clientOrderId", + "type": "u64" + }, + { + "name": "orderType", + "type": { + "defined": "PlaceOrderType" + } + }, + { + "name": "selfTradeBehavior", + "type": { + "defined": "SelfTradeBehavior" + } + }, + { + "name": "reduceOnly", + "type": "bool" + }, + { + "name": "expiryTimestamp", + "type": "u64" + }, + { + "name": "limit", + "type": "u8" + } + ], + "returns": { + "option": "u128" + } + }, { "name": "perpPlaceOrderPegged", "accounts": [ @@ -3701,6 +3807,120 @@ export type MangoV4 = { "option": "u128" } }, + { + "name": "perpPlaceOrderPeggedV2", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false, + "relations": [ + "group", + "bids", + "asks", + "event_queue", + "oracle" + ] + }, + { + "name": "bids", + "isMut": true, + "isSigner": false + }, + { + "name": "asks", + "isMut": true, + "isSigner": false + }, + { + "name": "eventQueue", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "side", + "type": { + "defined": "Side" + } + }, + { + "name": "priceOffsetLots", + "type": "i64" + }, + { + "name": "pegLimit", + "type": "i64" + }, + { + "name": "maxBaseLots", + "type": "i64" + }, + { + "name": "maxQuoteLots", + "type": "i64" + }, + { + "name": "clientOrderId", + "type": "u64" + }, + { + "name": "orderType", + "type": { + "defined": "PlaceOrderType" + } + }, + { + "name": "selfTradeBehavior", + "type": { + "defined": "SelfTradeBehavior" + } + }, + { + "name": "reduceOnly", + "type": "bool" + }, + { + "name": "expiryTimestamp", + "type": "u64" + }, + { + "name": "limit", + "type": "u8" + }, + { + "name": "maxOracleStalenessSlots", + "type": "i32" + } + ], + "returns": { + "option": "u128" + } + }, { "name": "perpCancelOrder", "accounts": [ @@ -7721,6 +7941,28 @@ export type MangoV4 = { ] } }, + { + "name": "SelfTradeBehavior", + "docs": [ + "Self trade behavior controls how taker orders interact with resting limit orders of the same account.", + "This setting has no influence on placing a resting or oracle pegged limit order that does not match", + "immediately, instead it's the responsibility of the user to correctly configure his taker orders." + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "DecrementTake" + }, + { + "name": "CancelProvide" + }, + { + "name": "AbortTransaction" + } + ] + } + }, { "name": "Side", "type": { @@ -9205,6 +9447,11 @@ export type MangoV4 = { "type": "i64", "index": false }, + { + "name": "totalQuoteLotsDecremented", + "type": "i64", + "index": false + }, { "name": "takerFeesPaid", "type": "i128", @@ -9553,6 +9800,11 @@ export type MangoV4 = { "code": 6047, "name": "InvalidHealthAccountCount", "msg": "incorrect number of health accounts" + }, + { + "code": 6048, + "name": "WouldSelfTrade", + "msg": "would self trade" } ] }; @@ -13152,6 +13404,112 @@ export const IDL: MangoV4 = { "option": "u128" } }, + { + "name": "perpPlaceOrderV2", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false, + "relations": [ + "group", + "bids", + "asks", + "event_queue", + "oracle" + ] + }, + { + "name": "bids", + "isMut": true, + "isSigner": false + }, + { + "name": "asks", + "isMut": true, + "isSigner": false + }, + { + "name": "eventQueue", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "side", + "type": { + "defined": "Side" + } + }, + { + "name": "priceLots", + "type": "i64" + }, + { + "name": "maxBaseLots", + "type": "i64" + }, + { + "name": "maxQuoteLots", + "type": "i64" + }, + { + "name": "clientOrderId", + "type": "u64" + }, + { + "name": "orderType", + "type": { + "defined": "PlaceOrderType" + } + }, + { + "name": "selfTradeBehavior", + "type": { + "defined": "SelfTradeBehavior" + } + }, + { + "name": "reduceOnly", + "type": "bool" + }, + { + "name": "expiryTimestamp", + "type": "u64" + }, + { + "name": "limit", + "type": "u8" + } + ], + "returns": { + "option": "u128" + } + }, { "name": "perpPlaceOrderPegged", "accounts": [ @@ -13260,6 +13618,120 @@ export const IDL: MangoV4 = { "option": "u128" } }, + { + "name": "perpPlaceOrderPeggedV2", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "account", + "isMut": true, + "isSigner": false, + "relations": [ + "group" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": true + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false, + "relations": [ + "group", + "bids", + "asks", + "event_queue", + "oracle" + ] + }, + { + "name": "bids", + "isMut": true, + "isSigner": false + }, + { + "name": "asks", + "isMut": true, + "isSigner": false + }, + { + "name": "eventQueue", + "isMut": true, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "side", + "type": { + "defined": "Side" + } + }, + { + "name": "priceOffsetLots", + "type": "i64" + }, + { + "name": "pegLimit", + "type": "i64" + }, + { + "name": "maxBaseLots", + "type": "i64" + }, + { + "name": "maxQuoteLots", + "type": "i64" + }, + { + "name": "clientOrderId", + "type": "u64" + }, + { + "name": "orderType", + "type": { + "defined": "PlaceOrderType" + } + }, + { + "name": "selfTradeBehavior", + "type": { + "defined": "SelfTradeBehavior" + } + }, + { + "name": "reduceOnly", + "type": "bool" + }, + { + "name": "expiryTimestamp", + "type": "u64" + }, + { + "name": "limit", + "type": "u8" + }, + { + "name": "maxOracleStalenessSlots", + "type": "i32" + } + ], + "returns": { + "option": "u128" + } + }, { "name": "perpCancelOrder", "accounts": [ @@ -17280,6 +17752,28 @@ export const IDL: MangoV4 = { ] } }, + { + "name": "SelfTradeBehavior", + "docs": [ + "Self trade behavior controls how taker orders interact with resting limit orders of the same account.", + "This setting has no influence on placing a resting or oracle pegged limit order that does not match", + "immediately, instead it's the responsibility of the user to correctly configure his taker orders." + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "DecrementTake" + }, + { + "name": "CancelProvide" + }, + { + "name": "AbortTransaction" + } + ] + } + }, { "name": "Side", "type": { @@ -18764,6 +19258,11 @@ export const IDL: MangoV4 = { "type": "i64", "index": false }, + { + "name": "totalQuoteLotsDecremented", + "type": "i64", + "index": false + }, { "name": "takerFeesPaid", "type": "i128", @@ -19112,6 +19611,11 @@ export const IDL: MangoV4 = { "code": 6047, "name": "InvalidHealthAccountCount", "msg": "incorrect number of health accounts" + }, + { + "code": 6048, + "name": "WouldSelfTrade", + "msg": "would self trade" } ] };