diff --git a/rust/crates/Cargo.lock b/rust/crates/Cargo.lock index fdfb49c..c27b0a6 100644 --- a/rust/crates/Cargo.lock +++ b/rust/crates/Cargo.lock @@ -2890,7 +2890,7 @@ dependencies = [ [[package]] name = "phoenix-sdk" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "async-trait", diff --git a/rust/crates/phoenix-sdk/Cargo.toml b/rust/crates/phoenix-sdk/Cargo.toml index 84d53dd..2751773 100644 --- a/rust/crates/phoenix-sdk/Cargo.toml +++ b/rust/crates/phoenix-sdk/Cargo.toml @@ -2,7 +2,7 @@ cargo-features = ["workspace-inheritance"] [package] name = "phoenix-sdk" -version = "0.7.0" +version = "0.7.1" description = "SDK for interacting with the Phoenix program" edition = "2021" license = "MIT" diff --git a/rust/crates/phoenix-sdk/src/ladder_utils.rs b/rust/crates/phoenix-sdk/src/ladder_utils.rs index 2c5d003..2ba0de8 100644 --- a/rust/crates/phoenix-sdk/src/ladder_utils.rs +++ b/rust/crates/phoenix-sdk/src/ladder_utils.rs @@ -1,4 +1,13 @@ -use phoenix::state::{markets::Ladder, Side}; +use std::ops::Deref; + +use phoenix::{ + quantities::WrapperU64, + state::{ + markets::{FIFOOrderId, FIFORestingOrder, Ladder, Market}, + OrderPacket, Side, + }, +}; +use solana_sdk::pubkey::Pubkey; #[derive(Debug, Clone)] pub struct SimulationSummaryInLots { @@ -6,29 +15,74 @@ pub struct SimulationSummaryInLots { pub quote_lots_filled: u64, } +impl Deref for LadderWithAdjustment { + type Target = Ladder; + fn deref(&self) -> &Self::Target { + &self.ladder + } +} + +pub struct LadderWithAdjustment { + ladder: Ladder, + tick_size_in_quote_lots_per_base_unit: u64, + base_lots_per_base_unit: u64, +} + +impl LadderWithAdjustment { + pub fn from_market( + market: &dyn Market, + ) -> Self { + Self { + ladder: market.get_ladder(u64::MAX), + tick_size_in_quote_lots_per_base_unit: market.get_tick_size().as_u64(), + base_lots_per_base_unit: market.get_base_lots_per_base_unit().as_u64(), + } + } + + pub fn from_market_with_expiration( + market: &dyn Market, + last_valid_slot: Option, + last_valid_unix_timestamp_in_seconds: Option, + ) -> Self { + Self { + ladder: market.get_ladder_with_expiration( + u64::MAX, + last_valid_slot, + last_valid_unix_timestamp_in_seconds, + ), + tick_size_in_quote_lots_per_base_unit: market.get_tick_size().as_u64(), + base_lots_per_base_unit: market.get_base_lots_per_base_unit().as_u64(), + } + } +} + pub trait MarketSimulator { fn sell_quote(&self, num_lots_quote: u64) -> SimulationSummaryInLots; fn sell_base(&self, num_lots_base: u64) -> SimulationSummaryInLots; fn simulate_market_sell(&self, side: Side, size_in_lots: u64) -> SimulationSummaryInLots; } -impl MarketSimulator for Ladder { +impl MarketSimulator for LadderWithAdjustment { fn sell_quote(&self, num_lots_quote: u64) -> SimulationSummaryInLots { - let mut remaining_quote_lots = num_lots_quote; + let adjusted_quote_lots = num_lots_quote * self.base_lots_per_base_unit; + let mut remaining_adjusted_quote_lots = adjusted_quote_lots; let mut base_lots = 0; for ask in self.asks.iter() { - if remaining_quote_lots == 0 { + if remaining_adjusted_quote_lots == 0 { break; } - let max_base_lots_you_can_buy = remaining_quote_lots / ask.price_in_ticks; + let max_base_lots_you_can_buy = remaining_adjusted_quote_lots + / (ask.price_in_ticks * self.tick_size_in_quote_lots_per_base_unit); let amount_lots_to_buy = max_base_lots_you_can_buy.min(ask.size_in_base_lots); base_lots += amount_lots_to_buy; - remaining_quote_lots -= amount_lots_to_buy * ask.price_in_ticks; + remaining_adjusted_quote_lots -= amount_lots_to_buy + * (ask.price_in_ticks * self.tick_size_in_quote_lots_per_base_unit); } - let quote_lots_used = num_lots_quote - remaining_quote_lots; + let quote_lots_used = + (adjusted_quote_lots - remaining_adjusted_quote_lots) / self.base_lots_per_base_unit; SimulationSummaryInLots { base_lots_filled: base_lots, quote_lots_filled: quote_lots_used, @@ -37,7 +91,7 @@ impl MarketSimulator for Ladder { fn sell_base(&self, num_lots_base: u64) -> SimulationSummaryInLots { let mut remaining_base_lots = num_lots_base; - let mut quote_lots = 0; + let mut adjusted_quote_lots = 0; for bid in self.bids.iter() { if remaining_base_lots == 0 { @@ -45,14 +99,15 @@ impl MarketSimulator for Ladder { } let lots_to_fill = remaining_base_lots.min(bid.size_in_base_lots); - quote_lots += lots_to_fill * bid.price_in_ticks; + adjusted_quote_lots += + lots_to_fill * bid.price_in_ticks * self.tick_size_in_quote_lots_per_base_unit; remaining_base_lots -= lots_to_fill; } let base_lots_used = num_lots_base - remaining_base_lots; SimulationSummaryInLots { base_lots_filled: base_lots_used, - quote_lots_filled: quote_lots, + quote_lots_filled: adjusted_quote_lots / self.base_lots_per_base_unit, } } @@ -66,11 +121,14 @@ impl MarketSimulator for Ladder { #[cfg(test)] mod test { + use crate::sdk_client::{self, SDKClient}; + use super::*; use phoenix::state::markets::LadderOrder; + use solana_sdk::{pubkey, signature::Keypair}; struct Fixture { - pub ladder: Ladder, + pub ladder: LadderWithAdjustment, pub atoms_in_base_lot: f64, pub atoms_in_quote_lot: f64, pub atoms_in_base_unit: f64, @@ -110,7 +168,11 @@ mod test { ], }; let fixture = Fixture { - ladder, + ladder: LadderWithAdjustment { + ladder, + tick_size_in_quote_lots_per_base_unit: 1000, + base_lots_per_base_unit: 1000, + }, atoms_in_base_lot: 1e6, atoms_in_quote_lot: 1., atoms_in_base_unit: 1e9, @@ -127,9 +189,13 @@ mod test { #[test] fn test_empty_ladder_sell() { - let ladder = Ladder { - bids: vec![], - asks: vec![], + let ladder = LadderWithAdjustment { + ladder: Ladder { + bids: vec![], + asks: vec![], + }, + tick_size_in_quote_lots_per_base_unit: 1000, + base_lots_per_base_unit: 1000, }; let result = ladder.simulate_market_sell(Side::Ask, 1000); assert_eq!(result.base_lots_filled, 0); @@ -155,15 +221,20 @@ mod test { let Fixture { ladder, .. } = get_sol_usdc_ladder(); // Compute the max lots you can buy (from available asks) - let max_lots_sellable: u64 = ladder + let max_lots_sellable = ladder .asks .iter() - .map(|ask| ask.size_in_base_lots * ask.price_in_ticks) - .sum(); + .map(|ask| { + ask.size_in_base_lots + * ask.price_in_ticks + * ladder.tick_size_in_quote_lots_per_base_unit + }) + .sum::() + / ladder.base_lots_per_base_unit; // Try to buy twice as much, which means you are selling twice as much base let to_sell = max_lots_sellable * 2; - let result = ladder.simulate_market_sell(Side::Bid, to_sell); + let result: SimulationSummaryInLots = ladder.simulate_market_sell(Side::Bid, to_sell); assert_eq!(result.quote_lots_filled, max_lots_sellable); assert!(result.quote_lots_filled > 0); @@ -217,4 +288,31 @@ mod test { ); } } + + #[tokio::test] + async fn test_with_mainnet_market() { + let mut sdk = SDKClient::new(&Keypair::new(), "https://api.mainnet-beta.solana.com") + .await + .unwrap(); + let solusdc = pubkey!("4DoNfFBfF7UokCC2FQzriy7yHK6DY6NVdYpuekQ5pRgg"); + sdk.add_market(&solusdc).await.unwrap(); + let sol = pubkey!("So11111111111111111111111111111111111111112"); + let usdc = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + let res = sdk + .simulate_market_transaction(&solusdc, &sol, 1_000_000_000, None) + .await + .unwrap(); + println!( + "Sell Price: {}", + (res.quote_atoms_filled as f64 / 1e6) / (res.base_atoms_filled as f64 / 1e9) + ); + let res = sdk + .simulate_market_transaction(&solusdc, &usdc, 1_000_000_000, None) + .await + .unwrap(); + println!( + "Buy Price: {}", + (res.quote_atoms_filled as f64 / 1e6) / (res.base_atoms_filled as f64 / 1e9) + ) + } } diff --git a/rust/crates/phoenix-sdk/src/sdk_client.rs b/rust/crates/phoenix-sdk/src/sdk_client.rs index d59b9f6..86cac69 100644 --- a/rust/crates/phoenix-sdk/src/sdk_client.rs +++ b/rust/crates/phoenix-sdk/src/sdk_client.rs @@ -1,4 +1,4 @@ -use crate::ladder_utils::{MarketSimulator, SimulationSummaryInLots}; +use crate::ladder_utils::{LadderWithAdjustment, MarketSimulator, SimulationSummaryInLots}; use crate::order_packet_template::ImmediateOrCancelOrderTemplate; use crate::order_packet_template::LimitOrderTemplate; use crate::order_packet_template::PostOnlyOrderTemplate; @@ -494,8 +494,8 @@ impl SDKClient { })? .inner; - let ladder = market.get_ladder_with_expiration( - u64::MAX, + let ladder = LadderWithAdjustment::from_market_with_expiration( + market, last_valid_slot, last_valid_unix_timestamp_in_seconds, );