Enable self-trading protection (#533)

Co-authored-by: Christian Kamm <mail@ckamm.de>
This commit is contained in:
Maximilian Schneider 2023-05-15 10:40:41 +02:00 committed by GitHub
parent 9166b761e9
commit c006948319
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1549 additions and 188 deletions

View File

@ -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
```

View File

@ -335,6 +335,7 @@ impl Rebalancer {
true, // reduce only
0,
10,
mango_v4::state::SelfTradeBehavior::DecrementTake,
)
.await?;
log::info!(

View File

@ -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<Instruction> {
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<Signature> {
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
}

View File

@ -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"
}
]
}

View File

@ -101,6 +101,8 @@ pub enum MangoError {
TokenInForceClose,
#[msg("incorrect number of health accounts")]
InvalidHealthAccountCount,
#[msg("would self trade")]
WouldSelfTrade,
}
impl MangoError {

View File

@ -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);

View File

@ -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<PerpPlaceOrder>,
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<Option<u128>> {
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<PerpPlaceOrder>,
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<Option<u128>> {
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()?,

View File

@ -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]

View File

@ -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::<u64>();
// Accrue maker fees immediately: they can be negative and applying them later

View File

@ -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(())
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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<AnyEvent> for FillEvent {
type Error = error::Error;
fn try_from(e: AnyEvent) -> Result<Self> {
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<Self> {
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<AnyEvent> for OutEvent {
type Error = error::Error;
fn try_from(e: AnyEvent) -> Result<Self> {
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<Self> {
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))
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
};

View File

@ -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"
}
]
};