-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from drift-labs/crispheaney/arb-perp
program: add arb_perp ix
- Loading branch information
Showing
11 changed files
with
938 additions
and
583 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
use anchor_lang::prelude::*; | ||
|
||
#[error_code] | ||
#[derive(PartialEq, Eq)] | ||
pub enum ErrorCode { | ||
#[msg("BidNotCrossed")] | ||
BidNotCrossed, | ||
#[msg("AskNotCrossed")] | ||
AskNotCrossed, | ||
#[msg("TakerOrderNotFound")] | ||
TakerOrderNotFound, | ||
#[msg("OrderSizeBreached")] | ||
OrderSizeBreached, | ||
#[msg("NoBestBid")] | ||
NoBestBid, | ||
#[msg("NoBestAsk")] | ||
NoBestAsk, | ||
#[msg("NoArbOpportunity")] | ||
NoArbOpportunity, | ||
#[msg("UnprofitableArb")] | ||
UnprofitableArb, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
use anchor_lang::prelude::*; | ||
use drift::controller::position::PositionDirection; | ||
use drift::cpi::accounts::PlaceAndTake; | ||
use drift::instructions::optional_accounts::{load_maps, AccountMaps}; | ||
use drift::instructions::{OrderParams, PostOnlyParam}; | ||
use drift::program::Drift; | ||
use drift::state::perp_market_map::MarketSet; | ||
|
||
use drift::math::orders::find_bids_and_asks_from_users; | ||
use drift::state::state::State; | ||
use drift::state::user::{MarketType, OrderTriggerCondition, OrderType, User, UserStats}; | ||
use drift::state::user_map::load_user_maps; | ||
|
||
use crate::error::ErrorCode; | ||
|
||
pub fn arb_perp<'info>( | ||
ctx: Context<'_, '_, '_, 'info, ArbPerp<'info>>, | ||
market_index: u16, | ||
) -> Result<()> { | ||
let clock = Clock::get()?; | ||
let slot = clock.slot; | ||
let now = clock.unix_timestamp; | ||
|
||
let taker = ctx.accounts.user.load()?; | ||
|
||
let quote_init = taker | ||
.get_perp_position(market_index) | ||
.map_or(0, |p| p.quote_asset_amount); | ||
|
||
let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); | ||
let AccountMaps { | ||
perp_market_map, | ||
mut oracle_map, | ||
.. | ||
} = load_maps( | ||
remaining_accounts_iter, | ||
&MarketSet::new(), | ||
&MarketSet::new(), | ||
slot, | ||
None, | ||
)?; | ||
|
||
let (makers, _) = load_user_maps(remaining_accounts_iter, true)?; | ||
|
||
let perp_market = perp_market_map.get_ref(&market_index)?; | ||
let oracle_price_data = oracle_map.get_price_data(&perp_market.amm.oracle)?; | ||
|
||
let (bids, asks) = | ||
find_bids_and_asks_from_users(&perp_market, oracle_price_data, &makers, slot, now)?; | ||
|
||
let best_bid = bids.first().ok_or(ErrorCode::NoBestBid)?; | ||
let best_ask = asks.first().ok_or(ErrorCode::NoBestAsk)?; | ||
|
||
if best_bid.price < best_ask.price { | ||
return Err(ErrorCode::NoArbOpportunity.into()); | ||
} | ||
|
||
let base_asset_amount = best_bid.base_asset_amount.min(best_ask.base_asset_amount); | ||
|
||
let get_order_params = |taker_direction: PositionDirection, taker_price: u64| -> OrderParams { | ||
OrderParams { | ||
order_type: OrderType::Limit, | ||
market_type: MarketType::Perp, | ||
direction: taker_direction, | ||
user_order_id: 0, | ||
base_asset_amount, | ||
price: taker_price, | ||
market_index, | ||
reduce_only: false, | ||
post_only: PostOnlyParam::None, | ||
immediate_or_cancel: true, | ||
max_ts: None, | ||
trigger_price: None, | ||
trigger_condition: OrderTriggerCondition::Above, | ||
oracle_price_offset: None, | ||
auction_duration: None, | ||
auction_start_price: None, | ||
auction_end_price: None, | ||
} | ||
}; | ||
|
||
let order_params = vec![ | ||
get_order_params(PositionDirection::Long, best_ask.price), | ||
get_order_params(PositionDirection::Short, best_bid.price), | ||
]; | ||
|
||
drop(taker); | ||
drop(perp_market); | ||
|
||
place_and_take(&ctx, order_params)?; | ||
|
||
let taker = ctx.accounts.user.load()?; | ||
let quote_end = taker | ||
.get_perp_position(market_index) | ||
.map_or(0, |p| p.quote_asset_amount); | ||
|
||
if quote_end <= quote_init { | ||
return Err(ErrorCode::NoArbOpportunity.into()); | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
#[derive(Accounts)] | ||
pub struct ArbPerp<'info> { | ||
pub state: Box<Account<'info, State>>, | ||
#[account(mut)] | ||
pub user: AccountLoader<'info, User>, | ||
#[account(mut)] | ||
pub user_stats: AccountLoader<'info, UserStats>, | ||
pub authority: Signer<'info>, | ||
pub drift_program: Program<'info, Drift>, | ||
} | ||
|
||
fn place_and_take<'info>( | ||
ctx: &Context<'_, '_, '_, 'info, ArbPerp<'info>>, | ||
orders_params: Vec<OrderParams>, | ||
) -> Result<()> { | ||
for order_params in orders_params { | ||
let drift_program = ctx.accounts.drift_program.to_account_info().clone(); | ||
let cpi_accounts = PlaceAndTake { | ||
state: ctx.accounts.state.to_account_info().clone(), | ||
user: ctx.accounts.user.to_account_info().clone(), | ||
user_stats: ctx.accounts.user_stats.to_account_info().clone(), | ||
authority: ctx.accounts.authority.to_account_info().clone(), | ||
}; | ||
|
||
let cpi_context = CpiContext::new(drift_program, cpi_accounts) | ||
.with_remaining_accounts(ctx.remaining_accounts.into()); | ||
|
||
drift::cpi::place_and_take_perp_order(cpi_context, order_params, None)?; | ||
} | ||
|
||
Ok(()) | ||
} |
123 changes: 123 additions & 0 deletions
123
programs/jit-proxy/src/instructions/check_order_constraints.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
use anchor_lang::prelude::*; | ||
use drift::instructions::optional_accounts::{load_maps, AccountMaps}; | ||
use drift::math::casting::Cast; | ||
use drift::math::safe_math::SafeMath; | ||
use drift::state::perp_market_map::MarketSet; | ||
use drift::state::user::User; | ||
|
||
use crate::error::ErrorCode; | ||
use crate::state::MarketType; | ||
|
||
pub fn check_order_constraints<'info>( | ||
ctx: Context<'_, '_, '_, 'info, CheckOrderConstraints<'info>>, | ||
constraints: Vec<OrderConstraint>, | ||
) -> Result<()> { | ||
let clock = Clock::get()?; | ||
let slot = clock.slot; | ||
|
||
let user = ctx.accounts.user.load()?; | ||
|
||
let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); | ||
let AccountMaps { | ||
perp_market_map, | ||
spot_market_map, | ||
mut oracle_map, | ||
} = load_maps( | ||
remaining_accounts_iter, | ||
&MarketSet::new(), | ||
&MarketSet::new(), | ||
slot, | ||
None, | ||
)?; | ||
|
||
for constraint in constraints.iter() { | ||
if constraint.market_type == MarketType::Spot { | ||
let spot_market = spot_market_map.get_ref(&constraint.market_index)?; | ||
let spot_position = match user.get_spot_position(constraint.market_index) { | ||
Ok(spot_position) => spot_position, | ||
Err(_) => continue, | ||
}; | ||
|
||
let signed_token_amount = spot_position | ||
.get_signed_token_amount(&spot_market)? | ||
.cast::<i64>()?; | ||
|
||
constraint.check( | ||
signed_token_amount, | ||
spot_position.open_bids, | ||
spot_position.open_asks, | ||
)?; | ||
} else { | ||
let perp_market = perp_market_map.get_ref(&constraint.market_index)?; | ||
let perp_position = match user.get_perp_position(constraint.market_index) { | ||
Ok(perp_position) => perp_position, | ||
Err(_) => continue, | ||
}; | ||
|
||
let oracle_price = oracle_map.get_price_data(&perp_market.amm.oracle)?.price; | ||
|
||
let settled_perp_position = | ||
perp_position.simulate_settled_lp_position(&perp_market, oracle_price)?; | ||
|
||
constraint.check( | ||
settled_perp_position.base_asset_amount, | ||
settled_perp_position.open_bids, | ||
settled_perp_position.open_asks, | ||
)?; | ||
} | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
#[derive(Accounts)] | ||
pub struct CheckOrderConstraints<'info> { | ||
pub user: AccountLoader<'info, User>, | ||
} | ||
|
||
#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] | ||
pub struct OrderConstraint { | ||
pub max_position: i64, | ||
pub min_position: i64, | ||
pub market_index: u16, | ||
pub market_type: MarketType, | ||
} | ||
|
||
impl OrderConstraint { | ||
pub fn check(&self, current_position: i64, open_bids: i64, open_asks: i64) -> Result<()> { | ||
let max_long = current_position.safe_add(open_bids)?; | ||
|
||
if max_long > self.max_position { | ||
msg!( | ||
"market index {} market type {:?}", | ||
self.market_index, | ||
self.market_type | ||
); | ||
msg!( | ||
"max long {} current position {} open bids {}", | ||
max_long, | ||
current_position, | ||
open_bids | ||
); | ||
return Err(ErrorCode::OrderSizeBreached.into()); | ||
} | ||
|
||
let max_short = current_position.safe_add(open_asks)?; | ||
if max_short < self.min_position { | ||
msg!( | ||
"market index {} market type {:?}", | ||
self.market_index, | ||
self.market_type | ||
); | ||
msg!( | ||
"max short {} current position {} open asks {}", | ||
max_short, | ||
current_position, | ||
open_asks | ||
); | ||
return Err(ErrorCode::OrderSizeBreached.into()); | ||
} | ||
|
||
Ok(()) | ||
} | ||
} |
Oops, something went wrong.