use fusionamm_client::{FusionPool, TickArray};
use fusionamm_core::{
get_order_book_side, get_tick_array_start_tick_index, invert_sqrt_price, price_to_sqrt_price, price_to_tick_index, sqrt_price_to_price,
sqrt_price_to_tick_index, tick_index_to_price, FusionPoolFacade, OrderBookEntry, TickArrayFacade, TickArraySequence, TickFacade, MAX_TICK_INDEX,
MIN_TICK_INDEX, TICK_ARRAY_SIZE,
};
use solana_pubkey::Pubkey;
use std::collections::HashMap;
const DEFAULT_ORDER_BOOK_ENTRIES: u32 = 12;
const TICK_EDGE_OFFSET: i32 = 10000;
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
struct TickArrayMock {
pub discriminator: [u8; 8],
pub start_tick_index: i32,
#[serde(with = "serde_with::As::<serde_with::DisplayFromStr>")]
pub fusion_pool: Pubkey,
#[serde(with = "serde_big_array::BigArray")]
pub ticks: [TickFacade; TICK_ARRAY_SIZE],
}
impl From<TickArrayMock> for TickArray {
fn from(mock: TickArrayMock) -> Self {
let ticks = mock.ticks.map(|t| {
if t.initialized {
fusionamm_client::MaybeTick::Initialized(fusionamm_client::TickData {
liquidity_net: t.liquidity_net,
liquidity_gross: t.liquidity_gross,
fee_growth_outside_a: t.fee_growth_outside_a,
fee_growth_outside_b: t.fee_growth_outside_b,
age: t.age,
open_orders_input: t.open_orders_input,
part_filled_orders_input: t.part_filled_orders_input,
part_filled_orders_remaining_input: t.part_filled_orders_remaining_input,
fulfilled_a_to_b_orders_input: t.fulfilled_a_to_b_orders_input,
fulfilled_b_to_a_orders_input: t.fulfilled_b_to_a_orders_input,
})
} else {
fusionamm_client::MaybeTick::Uninitialized
}
});
TickArray {
discriminator: mock.discriminator,
start_tick_index: mock.start_tick_index,
fusion_pool: mock.fusion_pool,
ticks,
}
}
}
#[derive(serde::Serialize, serde::Deserialize)]
struct PoolData {
mint_a_dec: u8,
mint_b_dec: u8,
pool: FusionPool,
tick_arrays: Vec<TickArray>,
}
#[test]
fn test_order_book_performance() {
let file = std::fs::File::open("src/tests/mocks/fusionpool_Aih57RRVos7Cw11vkBwtdjbmwLgJsHJ1BDNGX6V2ARnt.json").unwrap();
let mut pool_data: PoolData = serde_json::from_reader(file).unwrap();
pool_data.tick_arrays.sort_by_key(|k| k.start_tick_index);
let tick_arrays: Vec<TickArray> = pool_data.tick_arrays.into_iter().map(|t| t.into()).collect();
println!("PID: {}", std::process::id());
println!("Number of tick arrays: {}", tick_arrays.len());
let start = std::time::Instant::now();
calc_order_book(&pool_data.pool, &tick_arrays, (pool_data.mint_a_dec, pool_data.mint_b_dec));
println!("Done in {:?}", start.elapsed());
}
fn calc_order_book(pool: &FusionPool, tick_arrays: &[TickArray], decimals: (u8, u8)) {
let sqrt_price = pool.sqrt_price;
let pool_price = sqrt_price_to_price(sqrt_price, decimals.0, decimals.1);
if pool_price < 0.0 {
panic!("Pool price should be greater than zero");
}
let pool_price_inverted = 1.0 / pool_price;
let price_steps = get_order_book_price_steps_f64(pool_price);
let price_steps_inverted = get_order_book_price_steps_f64(pool_price_inverted);
let tick_refs: Vec<&TickArray> = tick_arrays.iter().collect();
let mut order_books = Vec::with_capacity(price_steps.len());
let mut order_books_inverted = Vec::with_capacity(price_steps_inverted.len());
for step in price_steps {
println!("Calculating order book: price step {}, entries {}", step, DEFAULT_ORDER_BOOK_ENTRIES);
order_books.push(OrderBook::new(step, false, DEFAULT_ORDER_BOOK_ENTRIES, pool, decimals, &tick_refs));
}
for step in price_steps_inverted {
println!("Calculating order book: inverted price step {}, entries {}", step, DEFAULT_ORDER_BOOK_ENTRIES);
order_books_inverted.push(OrderBook::new(step, true, DEFAULT_ORDER_BOOK_ENTRIES, pool, decimals, &tick_refs));
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct OrderBook {
pub price_step: f64,
pub bids: Vec<OrderBookEntry>,
pub asks: Vec<OrderBookEntry>,
}
impl OrderBook {
pub fn new(
price_step: f64,
inverted: bool,
entries: u32,
pool: &FusionPool,
(decimals_a, decimals_b): (u8, u8),
tick_arrays: &[&TickArray],
) -> Result<Self, fusionamm_core::CoreError> {
let fusion_pool_facade = FusionPoolFacade {
tick_spacing: pool.tick_spacing,
fee_rate: pool.fee_rate,
protocol_fee_rate: pool.protocol_fee_rate,
liquidity: pool.liquidity,
sqrt_price: pool.sqrt_price,
tick_current_index: pool.tick_current_index,
fee_growth_global_a: pool.fee_growth_global_a,
fee_growth_global_b: pool.fee_growth_global_b,
orders_total_amount_a: pool.orders_total_amount_a,
orders_total_amount_b: pool.orders_total_amount_b,
orders_filled_amount_a: pool.orders_filled_amount_a,
orders_filled_amount_b: pool.orders_filled_amount_b,
olp_fee_owed_a: pool.olp_fee_owed_a,
olp_fee_owed_b: pool.olp_fee_owed_b,
};
let price_shift = price_step * entries as f64;
let pool_price = if inverted {
sqrt_price_to_price(invert_sqrt_price(fusion_pool_facade.sqrt_price), decimals_b, decimals_a)
} else {
sqrt_price_to_price(fusion_pool_facade.sqrt_price, decimals_a, decimals_b)
};
let min_price = tick_index_to_price(MIN_TICK_INDEX + TICK_EDGE_OFFSET, decimals_a, decimals_b);
let max_price = tick_index_to_price(MAX_TICK_INDEX - TICK_EDGE_OFFSET, decimals_a, decimals_b);
let lower_price = min_price.max(pool_price - price_shift);
let upper_price = max_price.min(pool_price + price_shift);
let lower_tick_index = if inverted {
let sqrt_price = invert_sqrt_price(price_to_sqrt_price(upper_price, decimals_b, decimals_a));
sqrt_price_to_tick_index(sqrt_price)
} else {
price_to_tick_index(lower_price, decimals_a, decimals_b)
};
let upper_tick_index = if inverted {
let sqrt_price = invert_sqrt_price(price_to_sqrt_price(lower_price, decimals_b, decimals_a));
sqrt_price_to_tick_index(sqrt_price)
} else {
price_to_tick_index(upper_price, decimals_a, decimals_b)
};
let lower_tick_array_start_index = get_tick_array_start_tick_index(lower_tick_index, fusion_pool_facade.tick_spacing);
let upper_tick_array_start_index = get_tick_array_start_tick_index(upper_tick_index, fusion_pool_facade.tick_spacing);
let tick_arrays_map: HashMap<i32, &TickArray> = tick_arrays.iter().map(|tick_array| (tick_array.start_tick_index, *tick_array)).collect();
let tick_arrays_count =
(upper_tick_array_start_index - lower_tick_array_start_index) / (TICK_ARRAY_SIZE as i32 * fusion_pool_facade.tick_spacing as i32);
let mut matching_tick_arrays: Vec<TickArrayFacade> = Vec::new();
for i in 0..=tick_arrays_count {
let shift = i * TICK_ARRAY_SIZE as i32 * fusion_pool_facade.tick_spacing as i32;
let tick_array_start_index = lower_tick_array_start_index + shift;
let tick_array = tick_arrays_map.get(&tick_array_start_index);
if let Some(tick_array) = tick_array {
matching_tick_arrays.push((*tick_array).clone().into());
}
}
let tick_array_sequence = if matching_tick_arrays.is_empty() {
TickArraySequence::empty()
} else {
TickArraySequence::new(matching_tick_arrays, fusion_pool_facade.tick_spacing)?
};
let asks = get_order_book_side(&fusion_pool_facade, &tick_array_sequence, price_step, entries, inverted, decimals_a, decimals_b)?;
let bids = get_order_book_side(&fusion_pool_facade, &tick_array_sequence, price_step * -1.0, entries, inverted, decimals_a, decimals_b)?;
Ok(Self { price_step, bids, asks })
}
}
pub fn get_order_book_price_steps_f64(price: f64) -> Vec<f64> {
if price <= 0.0 {
panic!("price must be greater than 0");
}
let min_price_step = 1e-13;
let max_power = price.log10().floor() as i32;
let mut steps = Vec::new();
for i in 0..5 {
let power = max_power - i;
let step = 10f64.powi(power);
let triple_step = step * 3.0;
if i == 4 {
let quint = step * 5.0;
let double = step * 2.0;
if quint >= min_price_step {
steps.push(quint);
}
if double >= min_price_step {
steps.push(double);
}
}
if step < min_price_step {
continue;
}
if price < triple_step {
continue;
}
steps.push(step);
}
steps
}