use phoenix_rise::types::accounts::{
FifoOrderId, Orderbook, OrderbookRestingOrder, SplineCollection,
};
use solana_pubkey::Pubkey as PhoenixPubkey;
use super::super::math::{base_lots_to_units, ticks_to_price};
pub type SplineRow = (PhoenixPubkey, f64, f64, f64);
#[derive(Clone)]
pub struct ParsedSplineData {
pub bid_rows: Vec<SplineRow>,
pub ask_rows: Vec<SplineRow>,
pub bid_iceberg_markers: Vec<(f64, PhoenixPubkey)>,
pub ask_iceberg_markers: Vec<(f64, PhoenixPubkey)>,
pub best_bid: Option<f64>,
pub best_ask: Option<f64>,
pub best_bid_size: Option<f64>,
pub best_ask_size: Option<f64>,
}
#[inline]
fn load_collection(data: &[u8]) -> Option<SplineCollection> {
std::panic::catch_unwind(|| SplineCollection::try_from_account_bytes(data).ok()).ok()?
}
#[inline]
fn region_is_active(
region: &phoenix_rise::types::accounts::TickRegion,
current_slot: u64,
last_updated_slot: u64,
) -> bool {
if region.total_size <= region.filled_size {
return false;
}
region.lifespan.saturating_add(last_updated_slot) >= current_slot
}
pub fn parse_spline_sequence(data: &[u8]) -> Option<(u64, u64)> {
let collection = load_collection(data)?;
Some((
collection.sequence_number.sequence_number,
collection.sequence_number.last_update_slot,
))
}
fn expand_region<F>(
region: &phoenix_rise::types::accounts::TickRegion,
trader: solana_pubkey::Pubkey,
bld: i8,
price_at_offset: F,
out: &mut Vec<SplineRow>,
) where
F: Fn(u64) -> f64,
{
if region.start_offset >= region.end_offset {
return;
}
let unfilled_lots = region.total_size.saturating_sub(region.filled_size);
if unfilled_lots == 0 || region.density == 0 {
return;
}
let region_remaining_units = base_lots_to_units(unfilled_lots, bld);
let mut remaining = unfilled_lots;
for offset in (region.start_offset..region.end_offset).rev() {
if remaining == 0 {
break;
}
let take = remaining.min(region.density);
remaining -= take;
out.push((
trader,
price_at_offset(offset),
base_lots_to_units(take, bld),
region_remaining_units,
));
}
}
fn compute_cross_trim_skip(bid_rows: &[SplineRow], ask_rows: &[SplineRow]) -> (usize, usize) {
let mut bid_skip = 0usize;
let mut ask_skip = 0usize;
while let (Some(b), Some(a)) = (bid_rows.get(bid_skip), ask_rows.get(ask_skip)) {
if b.1 <= a.1 {
break;
}
if b.3 < a.3 {
bid_skip += 1;
} else {
ask_skip += 1;
}
}
(bid_skip, ask_skip)
}
pub fn parse_spline_data(
data: &[u8],
tick_size: u64,
bld: i8,
current_slot: u64,
) -> Option<ParsedSplineData> {
let collection = load_collection(data)?;
if std::env::var_os("CINDER_SPLINE_DEBUG").is_some() {
dump_spline_collection_debug(&collection, tick_size, bld);
}
let mut bid_rows: Vec<SplineRow> = Vec::new();
let mut ask_rows: Vec<SplineRow> = Vec::new();
let mut bid_iceberg_markers: Vec<(f64, PhoenixPubkey)> = Vec::new();
let mut ask_iceberg_markers: Vec<(f64, PhoenixPubkey)> = Vec::new();
for spline in collection.active_splines() {
let trader = spline.trader;
let mid_ticks = spline.mid_price;
let mid = ticks_to_price(mid_ticks, tick_size, bld);
let last_updated_slot = spline.user_update_slot;
let bid_start = (spline.bid_offset as usize).min(spline.bid_regions.len());
let bid_end = (spline.bid_num_regions as usize)
.min(spline.bid_regions.len())
.max(bid_start);
for region in &spline.bid_regions[bid_start..bid_end] {
if !region_is_active(region, current_slot, last_updated_slot) {
continue;
}
let price_at_offset = |offset| mid - ticks_to_price(offset, tick_size, bld);
if region.top_level_hidden_take_size > 0 {
bid_iceberg_markers.push((price_at_offset(region.end_offset), trader));
}
expand_region(region, trader, bld, price_at_offset, &mut bid_rows);
}
let ask_start = (spline.ask_offset as usize).min(spline.ask_regions.len());
let ask_end = (spline.ask_num_regions as usize)
.min(spline.ask_regions.len())
.max(ask_start);
for region in &spline.ask_regions[ask_start..ask_end] {
if !region_is_active(region, current_slot, last_updated_slot) {
continue;
}
let price_at_offset = |offset| mid + ticks_to_price(offset, tick_size, bld);
if region.top_level_hidden_take_size > 0 {
ask_iceberg_markers.push((price_at_offset(region.end_offset), trader));
}
expand_region(region, trader, bld, price_at_offset, &mut ask_rows);
}
}
bid_rows.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
ask_rows.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let (bid_skip, ask_skip) = compute_cross_trim_skip(&bid_rows, &ask_rows);
let bid_rows: Vec<SplineRow> = bid_rows.into_iter().skip(bid_skip).collect();
let ask_rows: Vec<SplineRow> = ask_rows.into_iter().skip(ask_skip).collect();
let best_bid = bid_rows.first().map(|r| r.1);
let best_ask = ask_rows.first().map(|r| r.1);
let best_bid_size = best_bid.map(|p| {
bid_rows
.iter()
.take_while(|r| r.1 == p)
.map(|r| r.2)
.sum::<f64>()
});
let best_ask_size = best_ask.map(|p| {
ask_rows
.iter()
.take_while(|r| r.1 == p)
.map(|r| r.2)
.sum::<f64>()
});
Some(ParsedSplineData {
bid_rows,
ask_rows,
bid_iceberg_markers,
ask_iceberg_markers,
best_bid,
best_ask,
best_bid_size,
best_ask_size,
})
}
fn dump_spline_collection_debug(collection: &SplineCollection, tick_size: u64, bld: i8) {
use std::fmt::Write as _;
let mut s = String::new();
let _ = writeln!(
s,
"asset={} num_splines={} num_active={} seq={} slot={}",
collection.asset_symbol,
collection.num_splines,
collection.num_active,
collection.sequence_number.sequence_number,
collection.sequence_number.last_update_slot,
);
for (i, spline) in collection.splines.iter().enumerate() {
if !spline.is_active {
continue;
}
let mid = ticks_to_price(spline.mid_price, tick_size, bld);
let trader_short: String = spline.trader.to_string().chars().take(8).collect();
let _ = writeln!(
s,
"spline[{i}] trader={trader_short} mid_ticks={} mid=${mid:.6} \
bid_offset={} bid_num_regions={} bid_filled={} \
ask_offset={} ask_num_regions={} ask_filled={}",
spline.mid_price,
spline.bid_offset,
spline.bid_num_regions,
spline.bid_filled_amount,
spline.ask_offset,
spline.ask_num_regions,
spline.ask_filled_amount,
);
let bid_end = (spline.bid_num_regions as usize).min(spline.bid_regions.len());
for (j, r) in spline.bid_regions.iter().enumerate().take(bid_end) {
let active = j >= spline.bid_offset as usize;
let p_start = mid - ticks_to_price(r.start_offset, tick_size, bld);
let p_end = mid - ticks_to_price(r.end_offset, tick_size, bld);
let _ = writeln!(
s,
" bid[{j}]{} start_off={} end_off={} ${p_start:.6}..${p_end:.6} \
density={} total={} filled={} hidden_filled={} top_hidden_take={} lifespan={}",
if active { "*" } else { " " },
r.start_offset,
r.end_offset,
r.density,
r.total_size,
r.filled_size,
r.hidden_filled_size,
r.top_level_hidden_take_size,
r.lifespan,
);
}
let ask_end = (spline.ask_num_regions as usize).min(spline.ask_regions.len());
for (j, r) in spline.ask_regions.iter().enumerate().take(ask_end) {
let active = j >= spline.ask_offset as usize;
let p_start = mid + ticks_to_price(r.start_offset, tick_size, bld);
let p_end = mid + ticks_to_price(r.end_offset, tick_size, bld);
let _ = writeln!(
s,
" ask[{j}]{} start_off={} end_off={} ${p_start:.6}..${p_end:.6} \
density={} total={} filled={} hidden_filled={} top_hidden_take={} lifespan={}",
if active { "*" } else { " " },
r.start_offset,
r.end_offset,
r.density,
r.total_size,
r.filled_size,
r.hidden_filled_size,
r.top_level_hidden_take_size,
r.lifespan,
);
}
}
let _ = std::fs::write("cinder_spline_debug.txt", s);
}
#[derive(Copy, Clone, Debug)]
pub struct L2Level {
pub price: f64,
pub qty: f64,
pub trader_id: u32,
}
#[inline]
fn aggregate_side<'a, I>(iter: I, tick_size: u64, bld: i8, max_prices: usize) -> Vec<L2Level>
where
I: Iterator<Item = (&'a FifoOrderId, &'a OrderbookRestingOrder)>,
{
let mut out: Vec<L2Level> = Vec::with_capacity(max_prices);
let mut cur_ticks: Option<u64> = None;
let mut cur_traders: Vec<(u32, f64)> = Vec::new();
let mut prices_seen: usize = 0;
let flush = |ticks: u64, traders: &mut Vec<(u32, f64)>, out: &mut Vec<L2Level>| {
let price = ticks_to_price(ticks, tick_size, bld);
for (trader_id, qty) in traders.drain(..) {
out.push(L2Level {
price,
qty,
trader_id,
});
}
};
for (order_id, order) in iter {
let ticks = order_id.price_in_ticks;
let trader_id = order.trader_position_id.trader_id.unwrap_or(0);
let qty = base_lots_to_units(order.num_base_lots_remaining, bld);
match cur_ticks {
Some(t) if t == ticks => {
if let Some(entry) = cur_traders.iter_mut().find(|(id, _)| *id == trader_id) {
entry.1 += qty;
} else {
cur_traders.push((trader_id, qty));
}
}
Some(t) => {
flush(t, &mut cur_traders, &mut out);
prices_seen += 1;
if prices_seen >= max_prices {
return out;
}
cur_ticks = Some(ticks);
cur_traders.push((trader_id, qty));
}
None => {
cur_ticks = Some(ticks);
cur_traders.push((trader_id, qty));
}
}
}
if let Some(t) = cur_ticks {
if prices_seen < max_prices {
flush(t, &mut cur_traders, &mut out);
}
}
out
}
fn resting_order_cmp_bid(
a: &OrderbookRestingOrder,
b: &OrderbookRestingOrder,
) -> std::cmp::Ordering {
b.initial_slot
.cmp(&a.initial_slot)
.then_with(|| a.next_node.cmp(&b.next_node))
}
fn resting_order_cmp_ask(
a: &OrderbookRestingOrder,
b: &OrderbookRestingOrder,
) -> std::cmp::Ordering {
a.initial_slot
.cmp(&b.initial_slot)
.then_with(|| a.next_node.cmp(&b.next_node))
}
pub fn parse_l2_book_from_market_account(
data: Vec<u8>,
tick_size: u64,
bld: i8,
max_prices: usize,
) -> Option<(Vec<L2Level>, Vec<L2Level>)> {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let ob = Orderbook::try_from_account_bytes(&data).ok()?;
let mut bid_entries: Vec<(&FifoOrderId, &OrderbookRestingOrder)> =
ob.bids.iter().map(|e| (&e.order_id, &e.order)).collect();
bid_entries.sort_by(|(ida, oa), (idb, ob)| {
idb.price_in_ticks
.cmp(&ida.price_in_ticks)
.then_with(|| resting_order_cmp_bid(oa, ob))
});
let mut ask_entries: Vec<(&FifoOrderId, &OrderbookRestingOrder)> =
ob.asks.iter().map(|e| (&e.order_id, &e.order)).collect();
ask_entries.sort_by(|(ida, oa), (idb, ob)| {
ida.price_in_ticks
.cmp(&idb.price_in_ticks)
.then_with(|| resting_order_cmp_ask(oa, ob))
});
let bids = aggregate_side(bid_entries.into_iter(), tick_size, bld, max_prices);
let asks = aggregate_side(ask_entries.into_iter(), tick_size, bld, max_prices);
Some((bids, asks))
}))
.ok()?
}
#[cfg(test)]
mod tests {
use super::*;
use solana_pubkey::Pubkey as PhoenixPubkey;
fn row(tag: u8, price: f64, size: f64) -> SplineRow {
(PhoenixPubkey::from([tag; 32]), price, size, size)
}
fn row_with_region_depth(tag: u8, price: f64, size: f64, region_remaining: f64) -> SplineRow {
(
PhoenixPubkey::from([tag; 32]),
price,
size,
region_remaining,
)
}
#[test]
fn cross_trim_keeps_locked_book_intact() {
let bids = vec![row(0xA1, 56.0, 5.0), row(0xA2, 55.0, 50.0)];
let asks = vec![row(0xB1, 56.0, 5.0), row(0xB2, 57.0, 30.0)];
let (bid_skip, ask_skip) = compute_cross_trim_skip(&bids, &asks);
assert_eq!(bid_skip, 0);
assert_eq!(ask_skip, 0);
}
#[test]
fn cross_trim_keeps_normal_book_intact() {
let bids = vec![row(0xA1, 55.0, 10.0), row(0xA2, 54.0, 20.0)];
let asks = vec![row(0xB1, 56.0, 10.0), row(0xB2, 57.0, 20.0)];
let (bid_skip, ask_skip) = compute_cross_trim_skip(&bids, &asks);
assert_eq!(bid_skip, 0);
assert_eq!(ask_skip, 0);
}
#[test]
fn cross_trim_drops_ghost_bid_above_real_ask() {
let bids = vec![row(0xA1, 57.0, 1.0), row(0xA2, 55.0, 50.0)];
let asks = vec![row(0xB1, 56.0, 10.0), row(0xB2, 57.0, 20.0)];
let (bid_skip, ask_skip) = compute_cross_trim_skip(&bids, &asks);
assert_eq!(bid_skip, 1);
assert_eq!(ask_skip, 0);
}
#[test]
fn cross_trim_drops_ghost_ask_below_real_bid() {
let bids = vec![row(0xA1, 55.0, 10.0), row(0xA2, 54.0, 50.0)];
let asks = vec![row(0xB1, 54.0, 1.0), row(0xB2, 56.0, 20.0)];
let (bid_skip, ask_skip) = compute_cross_trim_skip(&bids, &asks);
assert_eq!(bid_skip, 0);
assert_eq!(ask_skip, 1);
}
#[test]
fn cross_trim_handles_empty_sides() {
let (bid_skip, ask_skip) = compute_cross_trim_skip(&[], &[]);
assert_eq!(bid_skip, 0);
assert_eq!(ask_skip, 0);
let bids = vec![row(0xA1, 55.0, 10.0)];
let (bid_skip, ask_skip) = compute_cross_trim_skip(&bids, &[]);
assert_eq!(bid_skip, 0);
assert_eq!(ask_skip, 0);
}
#[test]
fn cross_trim_uses_region_depth_not_front_tick_size() {
let bids = vec![
row_with_region_depth(0xA1, 57.0, 1.0, 100.0),
row_with_region_depth(0xA2, 56.0, 33.0, 100.0),
row_with_region_depth(0xA3, 55.0, 33.0, 100.0),
row_with_region_depth(0xA4, 54.0, 33.0, 100.0),
];
let asks = vec![
row_with_region_depth(0xB1, 56.0, 50.0, 50.0),
];
let (bid_skip, ask_skip) = compute_cross_trim_skip(&bids, &asks);
assert_eq!(
bid_skip, 0,
"real bid region (depth 100) should outweigh ghost ask region (depth 50)"
);
assert_eq!(
ask_skip, 1,
"ghost ask should be trimmed, leaving touch at 57/(beyond ghost)"
);
}
}