Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rust/crates/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/crates/phoenix-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
136 changes: 117 additions & 19 deletions rust/crates/phoenix-sdk/src/ladder_utils.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,88 @@
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 {
pub base_lots_filled: u64,
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<Pubkey, FIFOOrderId, FIFORestingOrder, OrderPacket>,
) -> 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<Pubkey, FIFOOrderId, FIFORestingOrder, OrderPacket>,
last_valid_slot: Option<u64>,
last_valid_unix_timestamp_in_seconds: Option<u64>,
) -> 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,
Expand All @@ -37,22 +91,23 @@ 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 {
break;
}

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

Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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::<u64>()
/ 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);
Expand Down Expand Up @@ -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)
)
}
}
6 changes: 3 additions & 3 deletions rust/crates/phoenix-sdk/src/sdk_client.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
);
Expand Down