use crossterm::event::KeyEvent;
use optionstratlib::OptionStyle;
use optionstratlib::prelude::{Decimal, Positive};
use ratatui::Frame;
use ratatui::layout::{Alignment, Constraint, Flex, Layout, Rect};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::{Block, Cell, Paragraph, Row, Table};
use crate::app::keymap::{DepthAction, KeyChord, resolve_depth};
use crate::app::{LiveState, ScreenLoad};
use crate::chain::{DepthBook, DepthLevel, InstrumentKey, StreamHealth};
use crate::event::AppEvent;
use crate::ui::theme::{Theme, health_span, sanitize, spinner_frame};
const EM_DASH: &str = "—";
const PRICE_W: u16 = 12;
const SIZE_W: u16 = 12;
const SIDE_W: u16 = 5;
pub fn draw(state: &LiveState, frame: &mut Frame, area: Rect, theme: Theme, tick: u64) {
let selected = state.selected_depth_key();
let book = selected
.as_ref()
.and_then(|key| state.depth_store.book(key));
let block = Block::bordered().title(title_line(state, selected.as_ref(), book, theme));
let inner = block.inner(area);
frame.render_widget(block, area);
if !state.effective_capabilities().depth {
draw_unavailable(state, frame, inner, theme);
return;
}
match &state.load {
ScreenLoad::Loading => draw_loading(state, frame, inner, theme, tick),
ScreenLoad::Error { message } => draw_error(frame, inner, theme, message),
ScreenLoad::Ready => match (selected.as_ref(), book) {
(None, _) => draw_state_body(
frame,
inner,
Text::from(vec![
Line::from(Span::styled("select a contract", theme.dim())),
Line::from(Span::styled(
"focus a strike on the Chain screen (press 1)",
theme.dim(),
)),
]),
),
(Some(_), None) => draw_state_body(
frame,
inner,
Text::from(vec![
Line::from(Span::styled("no book yet", theme.dim())),
Line::from(Span::styled("waiting for the order book", theme.dim())),
]),
),
(Some(_), Some(book)) if book.level_count() == 0 => draw_state_body(
frame,
inner,
Text::from(vec![
Line::from(Span::styled("no book levels", theme.dim())),
Line::from(Span::styled("the venue sent an empty book", theme.dim())),
]),
),
(Some(_), Some(book)) => draw_ladder(state, frame, inner, theme, book),
},
}
}
#[must_use]
fn title_line(
state: &LiveState,
selected: Option<&InstrumentKey>,
book: Option<&DepthBook>,
theme: Theme,
) -> Line<'static> {
let mut spans = vec![Span::styled("Depth", theme.accent())];
if let Some(book) = book {
spans.push(Span::raw(" "));
spans.push(Span::raw(sanitize(&book.ladder().instrument.native_symbol)));
} else if let Some(key) = selected {
spans.push(Span::raw(" "));
spans.push(Span::raw(fmt_contract(key)));
}
if book.is_some_and(DepthBook::needs_resync) {
spans.push(Span::raw(" "));
spans.push(Span::styled("↻ resyncing", theme.warning()));
}
if !matches!(state.source.health, StreamHealth::Live) {
spans.push(Span::raw(" "));
spans.push(health_span(&state.source.health, theme));
}
Line::from(spans)
}
fn draw_loading(state: &LiveState, frame: &mut Frame, inner: Rect, theme: Theme, tick: u64) {
draw_state_body(
frame,
inner,
Text::from(vec![
Line::from(Span::styled(
format!(
"{} connecting to {}…",
spinner_frame(tick),
sanitize(state.source.provider.as_str())
),
theme.accent(),
)),
Line::from(Span::styled("waiting for the order book", theme.dim())),
]),
);
}
fn draw_error(frame: &mut Frame, inner: Rect, theme: Theme, message: &str) {
draw_state_body(
frame,
inner,
Text::from(vec![
Line::from(Span::styled(
format!("! {}", sanitize(message)),
theme.warning(),
)),
Line::from(Span::styled("press r to reconnect", theme.dim())),
]),
);
}
fn draw_unavailable(state: &LiveState, frame: &mut Frame, inner: Rect, theme: Theme) {
draw_state_body(
frame,
inner,
Text::from(vec![
Line::from(Span::styled(
format!(
"depth not available on {}",
sanitize(state.source.provider.as_str())
),
theme.warning(),
)),
Line::from(Span::styled(
"this venue exposes no option order book",
theme.dim(),
)),
]),
);
}
fn draw_ladder(state: &LiveState, frame: &mut Frame, inner: Rect, theme: Theme, book: &DepthBook) {
let ladder = book.ladder();
let (body_area, footer_area) = depth_body_split(inner);
let mut order: Vec<(&DepthLevel, bool)> = Vec::with_capacity(book.level_count());
order.extend(ladder.asks.iter().rev().map(|level| (level, false)));
order.extend(ladder.bids.iter().map(|level| (level, true)));
let visible = visible_rows_of(body_area);
let start = match state.depth_scroll {
Some(scroll) => clamp_start(scroll, visible, order.len()),
None => inside_market_anchor(book, visible),
};
let widths = [
Constraint::Length(PRICE_W),
Constraint::Length(SIZE_W),
Constraint::Length(SIDE_W),
];
let header = Row::new([
Cell::from(Line::from("Price").alignment(Alignment::Right)),
Cell::from(Line::from("Size").alignment(Alignment::Right)),
Cell::from(Line::from("Side").alignment(Alignment::Left)),
])
.style(theme.accent());
let rows: Vec<Row> = order
.iter()
.skip(start)
.take(visible)
.map(|&(level, is_bid)| ladder_row(level, is_bid, theme))
.collect();
let mut table = Table::new(rows, widths).header(header).column_spacing(1);
let stale = book.needs_resync() || !matches!(state.source.health, StreamHealth::Live);
if stale {
table = table.style(theme.dim());
}
frame.render_widget(table, body_area);
frame.render_widget(
Paragraph::new(footer_line(ladder.bids.first(), ladder.asks.first(), theme)),
footer_area,
);
}
#[must_use]
fn ladder_row(level: &DepthLevel, is_bid: bool, theme: Theme) -> Row<'static> {
let side_label = if is_bid { "bid" } else { "ask" };
Row::new([
Cell::from(Line::from(fmt_num(level.price)).alignment(Alignment::Right)),
Cell::from(Line::from(fmt_num(level.size)).alignment(Alignment::Right)),
Cell::from(Line::from(side_label).alignment(Alignment::Left)),
])
.style(theme.book_side_style(is_bid))
}
#[must_use]
fn footer_line(
best_bid: Option<&DepthLevel>,
best_ask: Option<&DepthLevel>,
theme: Theme,
) -> Line<'static> {
let text = match (best_bid, best_ask) {
(Some(bid), Some(ask)) => format!(
"bid {} / ask {} spread {}",
fmt_num(bid.price),
fmt_num(ask.price),
fmt_spread(ask.price, bid.price),
),
(Some(bid), None) => format!("bid {} (no asks)", fmt_num(bid.price)),
(None, Some(ask)) => format!("ask {} (no bids)", fmt_num(ask.price)),
(None, None) => "no levels".to_owned(),
};
Line::from(Span::styled(text, theme.dim()))
}
fn draw_state_body(frame: &mut Frame, inner: Rect, text: Text<'static>) {
let height = u16::try_from(text.height())
.unwrap_or(u16::MAX)
.min(inner.height);
let [centered] = Layout::vertical([Constraint::Length(height)])
.flex(Flex::Center)
.areas(inner);
frame.render_widget(Paragraph::new(text).alignment(Alignment::Center), centered);
}
const WHOLE_DECIMALS: usize = 2;
const MAX_DECIMALS: usize = 8;
#[must_use]
fn fmt_num(value: Positive) -> String {
if value == Positive::MAX {
return EM_DASH.to_owned();
}
let places = price_decimals(value.to_dec());
format!("{value:.places$}")
}
#[must_use]
fn price_decimals(value: Decimal) -> usize {
if value >= Decimal::ONE || value <= Decimal::ZERO {
return WHOLE_DECIMALS;
}
usize::try_from(value.scale())
.unwrap_or(MAX_DECIMALS)
.min(MAX_DECIMALS)
}
#[must_use]
fn fmt_spread(ask: Positive, bid: Positive) -> String {
if ask == Positive::MAX || bid == Positive::MAX {
return EM_DASH.to_owned();
}
match ask.to_dec().checked_sub(bid.to_dec()) {
Some(spread) if spread >= Decimal::ZERO => {
let places = price_decimals(spread);
format!("{spread:.places$}")
}
_ => EM_DASH.to_owned(),
}
}
#[must_use]
fn fmt_contract(key: &InstrumentKey) -> String {
let style = match key.style {
OptionStyle::Call => "C",
OptionStyle::Put => "P",
};
format!("{} {}", fmt_num(key.strike), style)
}
#[must_use]
fn floor_sub(a: usize, b: usize) -> usize {
a.max(b) - b
}
#[must_use]
fn depth_body_split(inner: Rect) -> (Rect, Rect) {
let [body, footer] = Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(inner);
(body, footer)
}
#[must_use]
fn visible_rows_of(body_area: Rect) -> usize {
floor_sub(usize::from(body_area.height), 1)
}
#[must_use]
pub fn body_visible_rows(area: Rect) -> usize {
let (body_area, _footer) = depth_body_split(Block::bordered().inner(area));
visible_rows_of(body_area)
}
#[must_use]
fn clamp_start(scroll: usize, visible: usize, total: usize) -> usize {
if visible == 0 || total <= visible {
return 0;
}
scroll.min(floor_sub(total, visible))
}
#[must_use]
fn inside_market_anchor(book: &DepthBook, visible: usize) -> usize {
let total = book.level_count();
if visible == 0 || total <= visible {
return 0;
}
let best_ask_index = floor_sub(book.ladder().asks.len(), 1);
floor_sub(best_ask_index, visible / 2).min(floor_sub(total, visible))
}
#[must_use]
pub fn handle_key(state: &mut LiveState, key: KeyEvent) -> Option<AppEvent> {
let chord = KeyChord::from_event(key)?;
match resolve_depth(chord)? {
DepthAction::Scroll => {
scroll(state, matches!(chord, KeyChord::Down | KeyChord::Char('j')));
None
}
}
}
fn scroll(state: &mut LiveState, down: bool) {
let visible = state.depth_visible_rows;
let Some((total, anchor)) = state
.selected_depth_key()
.as_ref()
.and_then(|key| state.depth_store.book(key))
.map(|book| (book.level_count(), inside_market_anchor(book, visible)))
else {
return;
};
let max = floor_sub(total, visible);
let current = state.depth_scroll.unwrap_or(anchor);
let next = if down {
current.checked_add(1).unwrap_or(current).min(max)
} else {
floor_sub(current, 1)
};
if next != current {
state.depth_scroll = Some(next);
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use chrono::{DateTime, Utc};
use crossterm::event::{KeyCode, KeyModifiers};
use optionstratlib::chains::OptionData;
use optionstratlib::chains::chain::OptionChain;
use optionstratlib::prelude::{Decimal, Positive};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::layout::Rect;
use ratatui::style::Modifier;
use super::{
body_visible_rows, clamp_start, draw, fmt_num, fmt_spread, handle_key, inside_market_anchor,
};
use crate::app::{LiveScreen, LiveState, ScreenLoad, SourceBinding};
use crate::chain::{
AliasCatalog, ChainFetch, ChainSource, ChainStore, ContractSpecFingerprint, DepthLadder,
DepthLevel, ExerciseStyle, ExpirySource, Instrument, InstrumentKey, ProviderId,
SettlementStyle, StreamHealth,
};
use crate::config::ThemeChoice;
use crate::providers::{ChainCapability, GreeksCapability, ProviderCapabilities};
use crate::ui::golden::buffer_to_text;
use crate::ui::theme::Theme;
const EXP: i64 = 1_700_000_000;
#[track_caller]
fn pid(id: &str) -> ProviderId {
match ProviderId::new(id) {
Ok(p) => p,
Err(e) => panic!("invalid provider id `{id}`: {e}"),
}
}
#[track_caller]
fn utc(secs: i64) -> DateTime<Utc> {
match DateTime::<Utc>::from_timestamp(secs, 0) {
Some(t) => t,
None => panic!("invalid test timestamp: {secs}"),
}
}
#[track_caller]
fn pos(value: f64) -> Positive {
match Positive::new(value) {
Ok(p) => p,
Err(e) => panic!("invalid test positive `{value}`: {e}"),
}
}
#[track_caller]
fn posd(mantissa: i64, scale: u32) -> Positive {
match Positive::new_decimal(Decimal::new(mantissa, scale)) {
Ok(p) => p,
Err(e) => panic!("invalid test decimal positive: {e}"),
}
}
fn row(strike: f64) -> OptionData {
let mut od = OptionData {
strike_price: pos(strike),
call_bid: Some(pos(1.0)),
call_ask: Some(pos(1.2)),
put_bid: Some(pos(2.0)),
put_ask: Some(pos(2.4)),
implied_volatility: pos(0.5),
..Default::default()
};
od.set_mid_prices();
od
}
fn store() -> ChainStore {
let mut chain = OptionChain::new("BTC", pos(60_000.0), "2025-06-27".to_owned(), None, None);
let _ = chain.options.insert(row(60_000.0));
let _ = chain.options.insert(row(62_000.0));
ChainStore::seed(
ChainFetch::new(
chain,
ExpirySource::new("BTC", utc(EXP), pid("deribit")),
AliasCatalog::new(),
),
ChainSource::Merged,
Duration::from_secs(2),
utc(EXP),
)
}
fn caps(depth: bool) -> ProviderCapabilities {
ProviderCapabilities::builder()
.chain(ChainCapability::Assemble)
.depth(depth)
.greeks(GreeksCapability::Provided)
.build()
}
fn depth_state(load: ScreenLoad, depth_capable: bool) -> LiveState {
let mut state = LiveState::new(
SourceBinding::new(pid("deribit"), caps(depth_capable), StreamHealth::Live),
store(),
);
state.screen = LiveScreen::Depth;
state.load = load;
state
}
fn ladder_for(
key: &InstrumentKey,
native_symbol: &str,
n_bids: usize,
n_asks: usize,
change_id: Option<u64>,
) -> DepthLadder {
let instrument = Instrument {
key: key.clone(),
provider: pid("deribit"),
native_symbol: native_symbol.to_owned(),
stream_symbol: None,
spec: ContractSpecFingerprint {
contract_multiplier: 1,
settlement: SettlementStyle::Cash,
exercise: ExerciseStyle::European,
quote_currency: "USD".to_owned(),
venue_product_code: "BTC".to_owned(),
},
};
let bids = (0..n_bids)
.map(|i| DepthLevel {
price: pos(60_000.0 - i as f64 * 10.0),
size: pos(1.0 + i as f64),
})
.collect();
let asks = (0..n_asks)
.map(|i| DepthLevel {
price: pos(60_010.0 + i as f64 * 10.0),
size: pos(1.0 + i as f64),
})
.collect();
DepthLadder {
instrument,
bids,
asks,
event_time: Some(utc(EXP)),
received_time: utc(EXP + 1),
change_id,
}
}
#[track_caller]
fn render_text(state: &LiveState, width: u16, height: u16) -> String {
let theme = Theme::resolve(ThemeChoice::Auto, false);
let mut term = match Terminal::new(TestBackend::new(width, height)) {
Ok(t) => t,
Err(e) => panic!("TestBackend construction failed: {e}"),
};
let area = Rect::new(0, 0, width, height);
match term.draw(|frame| draw(state, frame, area, theme, 0)) {
Ok(_) => {}
Err(e) => panic!("depth draw failed: {e}"),
}
buffer_to_text(term.backend().buffer())
}
fn press(state: &mut LiveState, code: KeyCode) {
let _ = handle_key(
state,
crossterm::event::KeyEvent::new(code, KeyModifiers::NONE),
);
}
fn populated_with(n_bids: usize, n_asks: usize) -> LiveState {
let mut state = depth_state(ScreenLoad::Ready, true);
state.selection.focused_row = Some(0);
if let Some(key) = state.selected_depth_key() {
let _ = state.depth_store.apply(ladder_for(
&key,
"BTC-27JUN25-60000-C",
n_bids,
n_asks,
Some(1),
));
}
state
}
#[track_caller]
fn body_is_dimmed(state: &LiveState, width: u16, height: u16) -> bool {
let theme = Theme::resolve(ThemeChoice::Auto, false);
let mut term = match Terminal::new(TestBackend::new(width, height)) {
Ok(t) => t,
Err(e) => panic!("TestBackend construction failed: {e}"),
};
let area = Rect::new(0, 0, width, height);
match term.draw(|frame| draw(state, frame, area, theme, 0)) {
Ok(_) => {}
Err(e) => panic!("depth draw failed: {e}"),
}
let buffer = term.backend().buffer();
for y in 2..(height / 2) {
for x in 0..width {
if let Some(cell) = buffer.cell((x, y))
&& cell.symbol().chars().any(|c| c.is_ascii_digit())
&& cell.modifier.contains(Modifier::DIM)
{
return true;
}
}
}
false
}
#[test]
fn test_render_states_never_panic() {
render_text(&depth_state(ScreenLoad::Loading, true), 120, 40);
render_text(
&depth_state(
ScreenLoad::Error {
message: "provider unreachable".to_owned(),
},
true,
),
120,
40,
);
render_text(&depth_state(ScreenLoad::Ready, true), 120, 40);
let mut focused = depth_state(ScreenLoad::Ready, true);
focused.selection.focused_row = Some(0);
render_text(&focused, 120, 40);
let populated = populated_with(3, 3);
render_text(&populated, 120, 40);
render_text(&populated, 40, 8);
let mut stale = populated_with(3, 3);
stale.source.health = StreamHealth::Stale { since: utc(EXP) };
render_text(&stale, 120, 40);
render_text(&stale, 40, 8);
let mut empty_book = depth_state(ScreenLoad::Ready, true);
empty_book.selection.focused_row = Some(0);
if let Some(key) = empty_book.selected_depth_key() {
let _ = empty_book.depth_store.apply(ladder_for(
&key,
"BTC-27JUN25-60000-C",
0,
0,
Some(1),
));
}
render_text(&empty_book, 120, 40);
render_text(&empty_book, 40, 8);
let mut resync = populated_with(3, 3);
if let Some(key) = resync.selected_depth_key() {
let _ =
resync
.depth_store
.apply(ladder_for(&key, "BTC-27JUN25-60000-C", 3, 3, Some(0)));
}
render_text(&resync, 40, 8);
render_text(&depth_state(ScreenLoad::Ready, false), 120, 40);
}
#[test]
fn test_unavailable_body_renders_honestly() {
let text = render_text(&depth_state(ScreenLoad::Ready, false), 120, 40);
assert!(
text.contains("not available"),
"the unavailable body names itself: {text:?}",
);
assert!(text.contains("deribit"), "it names the provider");
}
#[test]
fn test_select_a_contract_empty_state() {
let text = render_text(&depth_state(ScreenLoad::Ready, true), 120, 40);
assert!(
text.contains("select a contract"),
"the empty state nudges the user: {text:?}",
);
}
#[test]
fn test_no_book_yet_empty_state() {
let mut state = depth_state(ScreenLoad::Ready, true);
state.selection.focused_row = Some(0);
let text = render_text(&state, 120, 40);
assert!(
text.contains("no book yet"),
"a focused contract with no book shows the awaiting state: {text:?}",
);
}
#[test]
fn test_populated_ladder_shows_bid_ask_and_prices() {
let mut state = depth_state(ScreenLoad::Ready, true);
state.selection.focused_row = Some(0);
if let Some(key) = state.selected_depth_key() {
let _ = state
.depth_store
.apply(ladder_for(&key, "BTC-27JUN25-60000-C", 2, 2, Some(1)));
}
let text = render_text(&state, 120, 40);
assert!(text.contains("bid"), "the bid side label renders: {text:?}");
assert!(text.contains("ask"), "the ask side label renders");
assert!(text.contains("60000.00"), "a best-bid price renders");
assert!(text.contains("spread"), "the spread footer renders");
assert!(
text.contains("BTC-27JUN25-60000-C"),
"the instrument name is in the title",
);
}
#[test]
fn test_resync_badge_renders_on_change_id_gap() {
let mut state = depth_state(ScreenLoad::Ready, true);
state.selection.focused_row = Some(0);
if let Some(key) = state.selected_depth_key() {
let _ =
state
.depth_store
.apply(ladder_for(&key, "BTC-27JUN25-60000-C", 2, 2, Some(10)));
let _ = state
.depth_store
.apply(ladder_for(&key, "BTC-27JUN25-60000-C", 2, 2, Some(3)));
}
let text = render_text(&state, 120, 40);
assert!(
text.contains("resyncing"),
"a change_id gap badges the ladder resyncing: {text:?}",
);
}
#[test]
fn test_scroll_moves_and_clamps_to_the_viewport() {
let mut state = populated_with(5, 5);
state.depth_visible_rows = 4;
state.depth_scroll = Some(0);
press(&mut state, KeyCode::Down);
assert_eq!(state.depth_scroll, Some(1), "down scrolls one row");
press(&mut state, KeyCode::Up);
assert_eq!(state.depth_scroll, Some(0), "up scrolls back");
press(&mut state, KeyCode::Up);
assert_eq!(state.depth_scroll, Some(0), "up at the top clamps");
for _ in 0..50 {
press(&mut state, KeyCode::Down);
}
assert_eq!(
state.depth_scroll,
Some(6),
"down clamps at total − visible (the window lands exactly at the bottom)",
);
}
#[test]
fn test_j_k_scroll_like_the_chain_idiom() {
let mut state = populated_with(5, 5);
state.depth_visible_rows = 4;
state.depth_scroll = Some(0);
press(&mut state, KeyCode::Char('j'));
assert_eq!(state.depth_scroll, Some(1), "j scrolls down");
press(&mut state, KeyCode::Char('k'));
assert_eq!(state.depth_scroll, Some(0), "k scrolls up");
}
#[test]
fn test_at_limit_press_leaves_the_offset_unchanged() {
let mut state = populated_with(5, 5);
state.depth_visible_rows = 4; state.depth_scroll = Some(6); press(&mut state, KeyCode::Down);
assert_eq!(
state.depth_scroll,
Some(6),
"an at-limit down press is a no-op (no ViewSig flip)",
);
}
#[test]
fn test_a_fitting_book_never_scrolls() {
let mut state = populated_with(5, 5);
state.depth_visible_rows = 20; assert_eq!(state.depth_scroll, None, "starts unscrolled");
press(&mut state, KeyCode::Down);
assert_eq!(state.depth_scroll, None, "a fitting book stays unscrolled");
press(&mut state, KeyCode::Up);
assert_eq!(state.depth_scroll, None, "still unscrolled");
}
#[test]
fn test_scroll_is_a_noop_without_a_selected_book() {
let mut state = depth_state(ScreenLoad::Ready, true);
press(&mut state, KeyCode::Down);
assert_eq!(state.depth_scroll, None, "no book → scroll is a no-op");
}
#[test]
fn test_clamp_start_windows_without_over_scroll() {
assert_eq!(clamp_start(0, 5, 3), 0, "everything fits → start 0");
assert_eq!(clamp_start(9, 5, 3), 0, "over-scroll a short book → 0");
assert_eq!(clamp_start(2, 5, 20), 2, "in-range scroll passes through");
assert_eq!(clamp_start(999, 5, 20), 15, "clamps to total − visible");
assert_eq!(clamp_start(3, 0, 20), 0, "a zero window never indexes");
}
#[test]
fn test_inside_market_anchor_centers_and_reconciles_with_the_clamp() {
let state = populated_with(20, 20);
let book = match state
.selected_depth_key()
.as_ref()
.and_then(|key| state.depth_store.book(key))
{
Some(b) => b.clone(),
None => panic!("the seeded book must be retrievable"),
};
assert_eq!(
inside_market_anchor(&book, 4),
17,
"centers the inside market"
);
assert_eq!(
inside_market_anchor(&book, 40),
0,
"a fitting book anchors at 0"
);
assert_eq!(
inside_market_anchor(&book, 0),
0,
"an unknown viewport anchors at 0"
);
assert!(
inside_market_anchor(&book, 4) <= super::floor_sub(40, 4),
"the anchor reconciles with the scroll clamp",
);
}
#[test]
fn test_initial_view_centers_inside_market_on_a_deep_book() {
let state = populated_with(20, 20);
assert_eq!(state.depth_scroll, None, "unscrolled by default");
let text = render_text(&state, 40, 8);
assert!(
text.contains("60010.00"),
"the best ask is on-screen: {text:?}"
);
assert!(
text.contains("60000.00"),
"the best bid is on-screen: {text:?}"
);
assert!(
!text.contains("60200.00"),
"the worst ask is NOT shown (the view is centered, not top-anchored): {text:?}",
);
}
#[test]
fn test_body_visible_rows_matches_the_drawn_window() {
assert_eq!(body_visible_rows(Rect::new(0, 0, 40, 8)), 4);
assert_eq!(body_visible_rows(Rect::new(0, 0, 40, 1)), 0);
assert_eq!(body_visible_rows(Rect::new(0, 0, 40, 0)), 0);
}
#[test]
fn test_stale_health_dims_the_ladder_and_shows_the_badge() {
let mut state = populated_with(3, 3);
state.source.health = StreamHealth::Stale { since: utc(EXP) };
assert!(
body_is_dimmed(&state, 120, 40),
"a stale feed dims the ladder body",
);
let text = render_text(&state, 120, 40);
assert!(
text.contains("stale"),
"the stale badge renders in the title"
);
}
#[test]
fn test_live_health_leaves_the_ladder_bright() {
let state = populated_with(3, 3);
assert!(
!body_is_dimmed(&state, 120, 40),
"a live, continuous book renders bright",
);
}
#[test]
fn test_venue_instrument_name_renders_inert() {
let mut state = depth_state(ScreenLoad::Ready, true);
state.selection.focused_row = Some(0);
if let Some(key) = state.selected_depth_key() {
let hostile = "BTC\u{1b}]52;c;pwn\u{7}\u{1b}[2J-C";
let _ = state
.depth_store
.apply(ladder_for(&key, hostile, 2, 2, Some(1)));
}
let text = render_text(&state, 120, 40);
assert!(!text.contains('\u{1b}'), "no raw ESC survives to the title");
assert!(
text.contains("52;c;pwn"),
"the OSC params render as inert text",
);
}
#[test]
fn test_fmt_spread_guards_crossed_and_infinite() {
assert_eq!(fmt_spread(pos(60_010.0), pos(60_000.0)), "10.00");
assert_eq!(fmt_spread(pos(60_000.0), pos(60_010.0)), "—", "crossed → —");
assert_eq!(fmt_spread(Positive::MAX, pos(1.0)), "—", "inf → —");
assert_eq!(fmt_spread(posd(6, 2), posd(5, 2)), "0.01");
}
#[test]
fn test_fmt_num_scales_precision_to_magnitude() {
assert_eq!(fmt_num(posd(49, 3)), "0.049", "0.049 keeps its 3rd digit");
assert_eq!(fmt_num(posd(48, 3)), "0.048");
assert_eq!(fmt_num(posd(5, 2)), "0.05", "0.05 renders at scale 2");
assert_eq!(fmt_num(posd(61, 3)), "0.061");
assert_eq!(
fmt_num(posd(5, 3)),
"0.005",
"0.005 keeps its scale-3 digit"
);
assert_eq!(fmt_num(pos(1.0)), "1.00", "at 1.0 → cents");
assert_eq!(fmt_num(pos(60_000.0)), "60000.00", "index scale → cents");
assert_eq!(fmt_num(Positive::MAX), "—", "non-finite → em dash");
}
#[test]
fn test_fmt_num_distinguishes_near_one_sub_unit_prices() {
assert_eq!(fmt_num(posd(9994, 4)), "0.9994");
assert_eq!(fmt_num(posd(9995, 4)), "0.9995");
assert_ne!(
fmt_num(posd(9994, 4)),
fmt_num(posd(9995, 4)),
"distinct near-one prices render distinctly, not a shared 0.999",
);
let capped = fmt_num(posd(123_456_789, 9));
let decimals = capped
.split_once('.')
.map_or(0, |(_, frac)| frac.chars().count());
assert!(
decimals <= 8,
"a deep sub-unit scale is capped at MAX_DECIMALS: {capped:?}",
);
}
#[test]
fn test_draw_does_not_mutate_scroll() {
let mut state = populated_with(3, 3);
state.depth_scroll = Some(2);
let _ = render_text(&state, 120, 40);
assert_eq!(
state.depth_scroll,
Some(2),
"a draw must not move the scroll offset",
);
}
}