diff --git a/src/output/clob/books.rs b/src/output/clob/books.rs index aaecf33..ca8c195 100644 --- a/src/output/clob/books.rs +++ b/src/output/clob/books.rs @@ -1,5 +1,5 @@ use polymarket_client_sdk_v2::clob::types::response::{ - LastTradePriceResponse, LastTradesPricesResponse, OrderBookSummaryResponse, + LastTradePriceResponse, LastTradesPricesResponse, OrderBookSummaryResponse, OrderSummary, }; use serde_json::json; use tabled::settings::Style; @@ -7,6 +7,22 @@ use tabled::{Table, Tabled}; use crate::output::{DASH, OutputFormat, truncate}; +/// Returns bids in display order (best bid first). +/// +/// The CLOB API returns bids ascending by price, so the best (highest-priced) +/// bid is reversed to the top for human-facing display. +fn bids_in_display_order(bids: &[OrderSummary]) -> Vec<&OrderSummary> { + bids.iter().rev().collect() +} + +/// Returns asks in display order (best ask first). +/// +/// The CLOB API returns asks descending by price, so the best (lowest-priced) +/// ask is reversed to the top for human-facing display. +fn asks_in_display_order(asks: &[OrderSummary]) -> Vec<&OrderSummary> { + asks.iter().rev().collect() +} + pub fn print_order_book( result: &OrderBookSummaryResponse, output: &OutputFormat, @@ -35,9 +51,8 @@ pub fn print_order_book( println!("No bids."); } else { println!("Bids:"); - let rows: Vec = result - .bids - .iter() + let rows: Vec = bids_in_display_order(&result.bids) + .into_iter() .map(|o| Row { price: o.price.to_string(), size: o.size.to_string(), @@ -53,9 +68,8 @@ pub fn print_order_book( println!("No asks."); } else { println!("Asks:"); - let rows: Vec = result - .asks - .iter() + let rows: Vec = asks_in_display_order(&result.asks) + .into_iter() .map(|o| Row { price: o.price.to_string(), size: o.size.to_string(), @@ -158,3 +172,57 @@ pub fn print_last_trades_prices( } Ok(()) } + +#[cfg(test)] +mod tests { + use super::{OrderSummary, asks_in_display_order, bids_in_display_order}; + use rust_decimal_macros::dec; + + fn order(price: rust_decimal::Decimal) -> OrderSummary { + OrderSummary::builder().price(price).size(dec!(100)).build() + } + + // The CLOB API returns bids ascending (worst price first). + // The display order helper must reverse that so the best (highest) bid is first. + #[test] + fn bids_in_display_order_puts_best_bid_first() { + let api_order = vec![order(dec!(0.30)), order(dec!(0.40)), order(dec!(0.50))]; + + let displayed: Vec = bids_in_display_order(&api_order) + .into_iter() + .map(|o| o.price) + .collect(); + + assert_eq!( + displayed, + vec![dec!(0.50), dec!(0.40), dec!(0.30)], + "bids must be reversed for display so the highest-priced bid is on top" + ); + } + + // The CLOB API returns asks descending (worst price first). + // The display order helper must reverse that so the best (lowest) ask is first. + #[test] + fn asks_in_display_order_puts_best_ask_first() { + let api_order = vec![order(dec!(0.70)), order(dec!(0.60)), order(dec!(0.50))]; + + let displayed: Vec = asks_in_display_order(&api_order) + .into_iter() + .map(|o| o.price) + .collect(); + + assert_eq!( + displayed, + vec![dec!(0.50), dec!(0.60), dec!(0.70)], + "asks must be reversed for display so the lowest-priced ask is on top" + ); + } + + // Empty input must not panic and must produce an empty display order. + #[test] + fn empty_inputs_produce_empty_output() { + let empty: Vec = vec![]; + assert!(bids_in_display_order(&empty).is_empty()); + assert!(asks_in_display_order(&empty).is_empty()); + } +}