Skip to content
Open
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
82 changes: 75 additions & 7 deletions src/output/clob/books.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
use polymarket_client_sdk_v2::clob::types::response::{
LastTradePriceResponse, LastTradesPricesResponse, OrderBookSummaryResponse,
LastTradePriceResponse, LastTradesPricesResponse, OrderBookSummaryResponse, OrderSummary,
};
use serde_json::json;
use tabled::settings::Style;
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,
Expand Down Expand Up @@ -35,9 +51,8 @@ pub fn print_order_book(
println!("No bids.");
} else {
println!("Bids:");
let rows: Vec<Row> = result
.bids
.iter()
let rows: Vec<Row> = bids_in_display_order(&result.bids)
.into_iter()
.map(|o| Row {
price: o.price.to_string(),
size: o.size.to_string(),
Expand All @@ -53,9 +68,8 @@ pub fn print_order_book(
println!("No asks.");
} else {
println!("Asks:");
let rows: Vec<Row> = result
.asks
.iter()
let rows: Vec<Row> = asks_in_display_order(&result.asks)
.into_iter()
.map(|o| Row {
price: o.price.to_string(),
size: o.size.to_string(),
Expand Down Expand Up @@ -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<rust_decimal::Decimal> = 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<rust_decimal::Decimal> = 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<OrderSummary> = vec![];
assert!(bids_in_display_order(&empty).is_empty());
assert!(asks_in_display_order(&empty).is_empty());
}
}