use crate::display::format_large_number;
use std::ops::Bound;
use super::types::{
BookSide, BookSource, ExecutionEstimate, HealthCheck, OrderBook, OrderBookLevel, Trade,
};
use serde::{Deserialize, Serialize};
pub const DEFAULT_PEG_TARGET: f64 = 1.0;
pub const DEFAULT_PEG_RANGE: f64 = 0.001;
pub const DEFAULT_MIN_LEVELS: usize = 10;
pub(crate) fn quote_of(pair: &str) -> String {
crate::market::Pair::parse(pair)
.quote
.unwrap_or_else(|| "quote".to_string())
}
pub const DEFAULT_MIN_DEPTH: f64 = 3000.0;
pub const DEFAULT_MIN_BID_ASK_RATIO: f64 = 0.2;
pub const DEFAULT_MAX_BID_ASK_RATIO: f64 = 5.0;
pub const DEFAULT_MAX_SPREAD_PCT: f64 = 3.0;
pub const DEFAULT_MIN_TOP3_DEPTH: f64 = 300.0;
pub const DEFAULT_MIN_TOP10_DEPTH: f64 = 2000.0;
pub const DEFAULT_DEPTH_WINDOW_PCT: f64 = 2.0;
pub const DEFAULT_MIN_DEPTH_VOLUME_PCT: f64 = 5.0;
pub const DEFAULT_MAX_DEPTH_VOLUME_PCT: f64 = 10.0;
pub const DEFAULT_AMM_STEP_PCT: f64 = 0.1;
pub const TOP3_LEVELS: usize = 3;
pub const TOP10_LEVELS: usize = 10;
pub use super::types::{BPS_PER_PERCENT, PERCENT_PER_RATIO};
pub const PEG_OUTLIER_FACTOR: f64 = 5.0;
pub const SLIPPAGE_WARN_BPS: f64 = 10.0;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct HealthThresholds {
pub peg_target: f64,
pub peg_range: f64,
pub min_levels: usize,
pub min_depth: f64,
pub min_bid_ask_ratio: f64,
pub max_bid_ask_ratio: f64,
pub max_spread_pct: f64,
pub min_top3_depth: f64,
pub min_top10_depth: f64,
pub amm_step_pct: f64,
pub depth_window_pct: f64,
pub min_depth_volume_pct: f64,
pub max_depth_volume_pct: f64,
}
impl Default for HealthThresholds {
fn default() -> Self {
Self {
peg_target: DEFAULT_PEG_TARGET,
peg_range: DEFAULT_PEG_RANGE,
min_levels: DEFAULT_MIN_LEVELS,
min_depth: DEFAULT_MIN_DEPTH,
min_bid_ask_ratio: DEFAULT_MIN_BID_ASK_RATIO,
max_bid_ask_ratio: DEFAULT_MAX_BID_ASK_RATIO,
max_spread_pct: DEFAULT_MAX_SPREAD_PCT,
min_top3_depth: DEFAULT_MIN_TOP3_DEPTH,
min_top10_depth: DEFAULT_MIN_TOP10_DEPTH,
amm_step_pct: DEFAULT_AMM_STEP_PCT,
depth_window_pct: DEFAULT_DEPTH_WINDOW_PCT,
min_depth_volume_pct: DEFAULT_MIN_DEPTH_VOLUME_PCT,
max_depth_volume_pct: DEFAULT_MAX_DEPTH_VOLUME_PCT,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct HealthOverrides {
#[serde(alias = "peg")]
pub peg_target: Option<f64>,
pub peg_range: Option<f64>,
pub min_levels: Option<usize>,
pub min_depth: Option<f64>,
pub min_bid_ask_ratio: Option<f64>,
pub max_bid_ask_ratio: Option<f64>,
pub max_spread_pct: Option<f64>,
pub min_top3_depth: Option<f64>,
pub min_top10_depth: Option<f64>,
pub amm_step_pct: Option<f64>,
pub depth_window_pct: Option<f64>,
pub min_depth_volume_pct: Option<f64>,
pub max_depth_volume_pct: Option<f64>,
}
impl HealthThresholds {
pub fn peg_bands(&self) -> PegBands {
PegBands::new(self.peg_target, self.peg_range)
}
pub fn with_overrides(&self, o: &HealthOverrides) -> Self {
Self {
peg_target: o.peg_target.unwrap_or(self.peg_target),
peg_range: o.peg_range.unwrap_or(self.peg_range),
min_levels: o.min_levels.unwrap_or(self.min_levels),
min_depth: o.min_depth.unwrap_or(self.min_depth),
min_bid_ask_ratio: o.min_bid_ask_ratio.unwrap_or(self.min_bid_ask_ratio),
max_bid_ask_ratio: o.max_bid_ask_ratio.unwrap_or(self.max_bid_ask_ratio),
max_spread_pct: o.max_spread_pct.unwrap_or(self.max_spread_pct),
min_top3_depth: o.min_top3_depth.unwrap_or(self.min_top3_depth),
min_top10_depth: o.min_top10_depth.unwrap_or(self.min_top10_depth),
amm_step_pct: o.amm_step_pct.unwrap_or(self.amm_step_pct),
depth_window_pct: o.depth_window_pct.unwrap_or(self.depth_window_pct),
min_depth_volume_pct: o.min_depth_volume_pct.unwrap_or(self.min_depth_volume_pct),
max_depth_volume_pct: o.max_depth_volume_pct.unwrap_or(self.max_depth_volume_pct),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct TopDepth {
pub bid_top3: f64,
pub bid_top10: f64,
pub ask_top3: f64,
pub ask_top10: f64,
}
impl TopDepth {
fn from_book(book: &OrderBook, bands: &PegBands) -> Self {
let bid = |n| book.top_depth(BookSide::Bid, bands.eligible_band(BookSide::Bid), n);
let ask = |n| book.top_depth(BookSide::Ask, bands.eligible_band(BookSide::Ask), n);
Self {
bid_top3: bid(TOP3_LEVELS),
bid_top10: bid(TOP10_LEVELS),
ask_top3: ask(TOP3_LEVELS),
ask_top10: ask(TOP10_LEVELS),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PegBand {
OnPeg,
Slight,
Off,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PegBands {
target: f64,
range: f64,
}
impl PegBands {
pub fn new(target: f64, range: f64) -> Self {
Self { target, range }
}
pub fn target(&self) -> f64 {
self.target
}
pub fn outlier_width(&self) -> f64 {
self.range * PEG_OUTLIER_FACTOR
}
pub fn outlier_band(&self) -> (f64, f64) {
(
self.target - self.outlier_width(),
self.target + self.outlier_width(),
)
}
pub fn eligible_band(&self, side: BookSide) -> (Bound<f64>, Bound<f64>) {
let (lo, hi) = self.outlier_band();
match side {
BookSide::Bid => (Bound::Included(lo), Bound::Unbounded),
BookSide::Ask => (Bound::Unbounded, Bound::Included(hi)),
}
}
pub fn classify(&self, price: f64) -> PegBand {
let (lo, hi) = self.outlier_band();
if (price - self.target).abs() <= self.range {
PegBand::OnPeg
} else if (lo..=hi).contains(&price) {
PegBand::Slight
} else {
PegBand::Off
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PegStatus {
OnPeg,
BookUnhealthy,
SlightDeviation,
Deviation,
NoPrice,
}
fn depth_within(book: &OrderBook, mid: f64, window_pct: f64) -> (f64, bool) {
let lo = mid * (1.0 - window_pct / PERCENT_PER_RATIO);
let hi = mid * (1.0 + window_pct / PERCENT_PER_RATIO);
let depth =
book.depth_in_band(BookSide::Bid, lo..=hi) + book.depth_in_band(BookSide::Ask, lo..=hi);
let ends_inside = |levels: &[OrderBookLevel]| {
levels
.iter()
.rev()
.find(|l| l.is_valid())
.is_some_and(|l| l.price >= lo && l.price <= hi)
};
(depth, ends_inside(&book.bids) || ends_inside(&book.asks))
}
fn depth_volume_check(
book: &OrderBook,
thresholds: &HealthThresholds,
volume_24h: Option<f64>,
) -> HealthCheck {
let q = quote_of(&book.pair);
let w = thresholds.depth_window_pct;
let (lo, hi) = (
thresholds.min_depth_volume_pct,
thresholds.max_depth_volume_pct,
);
let (Some(volume), Some(mid)) = (volume_24h.filter(|v| *v > 0.0), book.mid_price()) else {
return HealthCheck::NotApplicable(
"Depth/volume: n/a (no 24h volume or mid price from this source)".to_string(),
);
};
let (depth, truncated) = depth_within(book, mid, w);
let pct = depth / volume * PERCENT_PER_RATIO;
let at_least = if truncated { "≥" } else { "" };
let msg = format!(
"Depth/volume: {at_least}{pct:.1}% ({at_least}{depth:.0} / {volume:.0} {q} within ±{w}%; target {lo}–{hi}%)"
);
if pct > hi {
HealthCheck::Warn(msg)
} else if pct < lo && truncated {
HealthCheck::NotApplicable(format!(
"{msg}; n/a: the book ends before ±{w}%, so the depth may be higher"
))
} else if pct < lo {
HealthCheck::Fail(msg)
} else {
HealthCheck::Pass(msg)
}
}
#[derive(Debug, Clone)]
pub struct MarketSummary {
pub pair: String,
pub source: BookSource,
pub peg_target: f64,
pub peg_range: f64,
pub best_bid: Option<f64>,
pub best_ask: Option<f64>,
pub mid_price: Option<f64>,
pub spread: Option<f64>,
pub spread_pct: Option<f64>,
pub top_depth: TopDepth,
pub volume_24h: Option<f64>,
pub execution_10k_buy: Option<ExecutionEstimate>,
pub execution_10k_sell: Option<ExecutionEstimate>,
pub asks: Vec<OrderBookLevel>,
pub bids: Vec<OrderBookLevel>,
pub ask_outliers: usize,
pub bid_outliers: usize,
pub ask_depth: f64,
pub bid_depth: f64,
pub checks: Vec<HealthCheck>,
pub healthy: bool,
pub bot_patterns: Option<super::patterns::BotPatterns>,
}
impl MarketSummary {
pub fn peg_deviation_pct(&self, price: f64) -> Option<f64> {
(self.peg_target > 0.0)
.then(|| (price - self.peg_target) / self.peg_target * PERCENT_PER_RATIO)
}
pub fn peg_deviation_bps(&self, price: f64) -> Option<f64> {
self.peg_deviation_pct(price)
.map(|pct| pct * BPS_PER_PERCENT)
}
pub fn peg_bands(&self) -> PegBands {
PegBands::new(self.peg_target, self.peg_range)
}
pub fn peg_status(&self) -> PegStatus {
let Some(mid) = self.mid_price else {
return PegStatus::NoPrice;
};
match self.peg_bands().classify(mid) {
PegBand::Off => PegStatus::Deviation,
PegBand::Slight => PegStatus::SlightDeviation,
PegBand::OnPeg if self.healthy => PegStatus::OnPeg,
PegBand::OnPeg => PegStatus::BookUnhealthy,
}
}
pub fn ask_levels(&self) -> usize {
self.asks.len()
}
pub fn bid_levels(&self) -> usize {
self.bids.len()
}
pub fn from_order_book(
book: &OrderBook,
thresholds: &HealthThresholds,
volume_24h: Option<f64>,
) -> Self {
let q = quote_of(&book.pair);
let peg_target = thresholds.peg_target;
let bands = thresholds.peg_bands();
let (ask_band, bid_band) = (
bands.eligible_band(BookSide::Ask),
bands.eligible_band(BookSide::Bid),
);
let asks: Vec<OrderBookLevel> = book
.valid_levels_in_band(BookSide::Ask, ask_band)
.cloned()
.collect();
let bids: Vec<OrderBookLevel> = book
.valid_levels_in_band(BookSide::Bid, bid_band)
.cloned()
.collect();
let outliers =
|side, eligible: usize| book.valid_levels_in_band(side, ..).count() - eligible;
let ask_outliers = outliers(BookSide::Ask, asks.len());
let bid_outliers = outliers(BookSide::Bid, bids.len());
let ask_depth = book.depth_in_band(BookSide::Ask, ask_band);
let bid_depth = book.depth_in_band(BookSide::Bid, bid_band);
let top_depth = TopDepth::from_book(book, &bands);
let mut checks = Vec::new();
let below_peg: Vec<_> = asks.iter().filter(|a| a.price < peg_target).collect();
if below_peg.is_empty() {
checks.push(HealthCheck::Pass("No sells below peg".to_string()));
} else {
let usdt = below_peg.iter().map(|a| a.value()).sum::<f64>();
checks.push(HealthCheck::Fail(format!(
"{} sell(s) below peg ({:.0} {q})",
below_peg.len(),
usdt
)));
}
let ratio = if ask_depth > 0.0 {
bid_depth / ask_depth
} else {
0.0
};
if ratio < thresholds.min_bid_ask_ratio || ratio > thresholds.max_bid_ask_ratio {
checks.push(HealthCheck::Fail(format!("Bid/Ask ratio: {:.2}x", ratio)));
} else {
checks.push(HealthCheck::Pass(format!("Bid/Ask ratio: {:.2}x", ratio)));
}
let amm = book.source == BookSource::SyntheticAmm;
match (amm, book.spread_pct()) {
(true, _) => checks.push(HealthCheck::NotApplicable(
"Spread: n/a (synthetic AMM book; the real spread is the pool fee, \
which the data source does not report)"
.to_string(),
)),
(false, Some(spread_pct)) => {
let msg = format!(
"Spread: {:.3}% (max {:.1}%)",
spread_pct, thresholds.max_spread_pct
);
if spread_pct > thresholds.max_spread_pct {
checks.push(HealthCheck::Fail(msg));
} else {
checks.push(HealthCheck::Pass(msg));
}
}
(false, None) => {
checks.push(HealthCheck::Fail("Spread: no valid bid or ask".to_string()))
}
}
if amm {
checks.push(HealthCheck::NotApplicable(
"Bid levels: n/a (synthetic AMM book; the step size sets the level count)"
.to_string(),
));
} else if bids.len() < thresholds.min_levels {
checks.push(HealthCheck::Fail(format!(
"Bid levels: {} < {} minimum",
bids.len(),
thresholds.min_levels
)));
} else {
checks.push(HealthCheck::Pass(format!(
"Bid levels: {} (min {})",
bids.len(),
thresholds.min_levels
)));
}
if bid_depth < thresholds.min_depth {
checks.push(HealthCheck::Fail(format!(
"Bid depth: {:.0} {q} < {:.0} {q} minimum",
bid_depth, thresholds.min_depth
)));
} else {
checks.push(HealthCheck::Pass(format!(
"Bid depth: {:.0} {q} (min {:.0})",
bid_depth, thresholds.min_depth
)));
}
if amm {
checks.push(HealthCheck::NotApplicable(
"Ask levels: n/a (synthetic AMM book; the step size sets the level count)"
.to_string(),
));
} else if asks.len() < thresholds.min_levels {
checks.push(HealthCheck::Fail(format!(
"Ask levels: {} < {} minimum",
asks.len(),
thresholds.min_levels
)));
} else {
checks.push(HealthCheck::Pass(format!(
"Ask levels: {} (min {})",
asks.len(),
thresholds.min_levels
)));
}
if ask_depth < thresholds.min_depth {
checks.push(HealthCheck::Fail(format!(
"Ask depth: {:.0} {q} < {:.0} {q} minimum",
ask_depth, thresholds.min_depth
)));
} else {
checks.push(HealthCheck::Pass(format!(
"Ask depth: {:.0} {q} (min {:.0})",
ask_depth, thresholds.min_depth
)));
}
for (side, top3, top10) in [
("Bid", top_depth.bid_top3, top_depth.bid_top10),
("Ask", top_depth.ask_top3, top_depth.ask_top10),
] {
for (n, depth, min) in [
(TOP3_LEVELS, top3, thresholds.min_top3_depth),
(TOP10_LEVELS, top10, thresholds.min_top10_depth),
] {
if depth < min {
checks.push(HealthCheck::Fail(format!(
"{side} top-{n} depth: {depth:.0} {q} < {min:.0} {q} minimum"
)));
} else {
checks.push(HealthCheck::Pass(format!(
"{side} top-{n} depth: {depth:.0} {q} (min {min:.0})"
)));
}
}
}
checks.push(depth_volume_check(book, thresholds, volume_24h));
let healthy = !checks.iter().any(|c| matches!(c, HealthCheck::Fail(_)));
let execution_10k_buy = book.estimate_buy_execution(10_000.0);
let execution_10k_sell = book.estimate_sell_execution(10_000.0);
Self {
pair: book.pair.clone(),
source: book.source,
peg_target,
peg_range: thresholds.peg_range,
best_bid: book.best_bid(),
best_ask: book.best_ask(),
mid_price: book.mid_price(),
spread: book.spread(),
spread_pct: book.spread_pct(),
top_depth,
volume_24h,
execution_10k_buy,
execution_10k_sell,
asks,
bids,
ask_outliers,
bid_outliers,
ask_depth,
bid_depth,
checks,
healthy,
bot_patterns: None,
}
}
pub fn check_bot_patterns(&mut self, trades: std::result::Result<&[Trade], &str>) {
let book = (self.source == BookSource::Exchange)
.then_some((self.bids.as_slice(), self.asks.as_slice()));
let found = super::patterns::detect(trades, book);
self.checks.push(found.check());
self.bot_patterns = Some(found);
}
pub fn check_depth_cycles(&mut self, bid: &[(i64, f64)], ask: &[(i64, f64)]) {
let Some(found) = self.bot_patterns.as_mut() else {
return;
};
found.set_depth_cycles(bid, ask);
let check = found.check();
let ours = |c: &HealthCheck| {
let m = match c {
HealthCheck::Pass(m) | HealthCheck::Warn(m) | HealthCheck::NotApplicable(m) => m,
_ => return false,
};
m.starts_with("Bot patterns:") || m.starts_with("No bot patterns")
};
match self.checks.iter_mut().find(|c| ours(c)) {
Some(c) => *c = check,
None => self.checks.push(check),
}
}
pub fn to_json(&self) -> serde_json::Value {
let exec = |e: &ExecutionEstimate| {
serde_json::json!({
"notional_usdt": e.notional_usdt,
"vwap": e.vwap,
"slippage_bps": e.slippage_bps,
"fillable": e.fillable,
})
};
let levels = |ls: &[OrderBookLevel]| {
ls.iter()
.take(20)
.map(|l| {
serde_json::json!({"price": l.price, "quantity": l.quantity, "value": l.value()})
})
.collect::<Vec<_>>()
};
serde_json::json!({
"pair": self.pair,
"quote": quote_of(&self.pair),
"book_source": self.source,
"peg_target": self.peg_target,
"best_bid": self.best_bid,
"best_ask": self.best_ask,
"mid_price": self.mid_price,
"spread": self.spread,
"volume_24h": self.volume_24h,
"bid_depth": self.bid_depth,
"ask_depth": self.ask_depth,
"bid_outliers": self.bid_outliers,
"ask_outliers": self.ask_outliers,
"bid_levels": self.bid_levels(),
"ask_levels": self.ask_levels(),
"healthy": self.healthy,
"slippage_warn_bps": SLIPPAGE_WARN_BPS,
"execution_10k_buy": self.execution_10k_buy.as_ref().map(exec),
"execution_10k_sell": self.execution_10k_sell.as_ref().map(exec),
"bids": levels(&self.bids),
"asks": levels(&self.asks),
"checks": &self.checks,
"bot_patterns": &self.bot_patterns,
})
}
fn price_with_peg_deviation(&self, price: f64) -> String {
use crate::display::terminal as t;
let shown = t::format_price_peg(price, &self.peg_bands());
match self.peg_deviation_pct(price) {
Some(pct) => format!("{shown} ({pct:+.3}%)"),
None => shown,
}
}
pub fn format_text(&self, venue_or_chain: Option<&str>) -> String {
self.format_text_with_tokens(venue_or_chain, &[])
}
pub fn format_text_with_tokens(
&self,
venue_or_chain: Option<&str>,
tokens: &[super::assets::TokenAddress],
) -> String {
let q = quote_of(&self.pair);
use crate::display::terminal as t;
let mut out = String::new();
let title = match venue_or_chain {
Some(c) => format!("{} ({})", self.pair, c),
None => self.pair.clone(),
};
out.push_str(&t::section_header(&title));
out.push('\n');
for token in tokens {
let mut value = token.contract.clone().unwrap_or("native coin".into());
if token.other_networks > 0 {
value.push_str(&format!(" (+{} networks)", token.other_networks));
}
let label = format!("{} ({})", token.symbol, token.network);
out.push_str(&t::kv_row(&label, &value));
out.push('\n');
}
out.push_str(&t::subsection_header("Metrics"));
out.push('\n');
if let Some(c) = venue_or_chain {
out.push_str(&t::kv_row("Venue", c));
out.push('\n');
}
out.push_str(&t::kv_row("Book", self.source.label()));
out.push('\n');
let d = crate::display::format::price_decimals(self.peg_target);
out.push_str(&t::kv_row(
"Peg Target",
&format!("{:.d$}", self.peg_target),
));
out.push('\n');
if let Some(bb) = self.best_bid {
out.push_str(&t::kv_row("Best Bid", &self.price_with_peg_deviation(bb)));
out.push('\n');
}
if let Some(ba) = self.best_ask {
out.push_str(&t::kv_row("Best Ask", &self.price_with_peg_deviation(ba)));
out.push('\n');
}
if let Some(mid) = self.mid_price {
out.push_str(&t::kv_row("Mid Price", &self.price_with_peg_deviation(mid)));
out.push('\n');
}
if let (Some(spread), Some(pct)) = (self.spread, self.spread_pct) {
out.push_str(&t::kv_row("Spread", &format!("{spread:.d$} ({pct:.3}%)")));
out.push('\n');
}
if self.ask_outliers + self.bid_outliers > 0 {
out.push_str(&t::kv_row(
"Note",
"best prices include levels outside the peg band; depth and levels do not",
));
out.push('\n');
}
if let Some(v) = self.volume_24h {
let label = if self.source == BookSource::Exchange {
"Volume (24h)"
} else {
"Volume (24h, best pool)"
};
out.push_str(&t::kv_row(
label,
&format!("{} {q}", format_large_number(v)),
));
out.push('\n');
}
if let Some(e) = &self.execution_10k_buy {
let msg = if e.fillable {
format!("~{:.2} bps slippage", e.slippage_bps)
} else {
"insufficient liquidity".to_string()
};
out.push_str(&t::kv_row("Exec 10K buy", &msg));
out.push('\n');
}
if let Some(e) = &self.execution_10k_sell {
let msg = if e.fillable {
format!("~{:.2} bps slippage", e.slippage_bps)
} else {
"insufficient liquidity".to_string()
};
out.push_str(&t::kv_row("Exec 10K sell", &msg));
out.push('\n');
}
let base_symbol = self.pair.split('/').next().unwrap_or("BASE");
let ask_label = if self.ask_outliers > 0 {
format!(
"Ask Side — {} levels, {} {q} (+{} outliers excl.)",
self.ask_levels(),
format_large_number(self.ask_depth),
self.ask_outliers
)
} else {
format!(
"Ask Side — {} levels, {} {q}",
self.ask_levels(),
format_large_number(self.ask_depth)
)
};
out.push_str(&t::subsection_header(&ask_label));
out.push('\n');
for level in self.asks.iter().take(8) {
out.push_str(&t::orderbook_level(
level.price,
level.quantity,
base_symbol,
level.value(),
&q,
&self.peg_bands(),
));
out.push('\n');
}
if self.asks.len() > 8 {
out.push_str(&t::blank_row());
out.push_str(&format!(" ... +{} more levels\n", self.asks.len() - 8));
}
let bid_label = if self.bid_outliers > 0 {
format!(
"Bid Side — {} levels, {} {q} (+{} outliers excl.)",
self.bid_levels(),
format_large_number(self.bid_depth),
self.bid_outliers
)
} else {
format!(
"Bid Side — {} levels, {} {q}",
self.bid_levels(),
format_large_number(self.bid_depth)
)
};
out.push_str(&t::subsection_header(&bid_label));
out.push('\n');
for level in self.bids.iter().take(8) {
out.push_str(&t::orderbook_level(
level.price,
level.quantity,
base_symbol,
level.value(),
&q,
&self.peg_bands(),
));
out.push('\n');
}
if self.bids.len() > 8 {
out.push_str(&t::blank_row());
out.push_str(&format!(" ... +{} more levels\n", self.bids.len() - 8));
}
if let Some(found) = self
.bot_patterns
.as_ref()
.filter(|b| !b.findings.is_empty())
{
out.push_str(&found.format_text());
}
out.push_str(&t::subsection_header("Health Checks"));
out.push('\n');
for check in &self.checks {
out.push_str(&check.render_text());
out.push('\n');
}
out.push_str(&t::blank_row());
out.push('\n');
out.push_str(&t::status_line(self.healthy));
out.push('\n');
out.push_str(&t::section_footer());
out.push('\n');
out
}
pub fn format_markdown(&self, venue: Option<&str>) -> String {
let q = quote_of(&self.pair);
let d = crate::display::format::price_decimals(self.peg_target);
let with_deviation = |price: f64| match self.peg_deviation_pct(price) {
Some(pct) => format!("{price:.d$} ({pct:+.3}%)"),
None => format!("{price:.d$}"),
};
let bid_ask_or_dash =
|price: Option<f64>| price.map(with_deviation).unwrap_or_else(|| "-".to_string());
let price_or_dash = |price: Option<f64>| {
price
.map(|p| format!("{p:.d$}"))
.unwrap_or_else(|| "-".to_string())
};
let exec_or_dash = |e: &Option<ExecutionEstimate>| {
e.as_ref()
.map(|e| {
if e.fillable {
format!("{:.2} bps", e.slippage_bps)
} else {
"insufficient".to_string()
}
})
.unwrap_or_else(|| "-".to_string())
};
let mut md = String::new();
if let Some(v) = venue {
md.push_str(&format!("**Venue:** {v} \n\n"));
}
md.push_str("| Metric | Value |\n|--------|-------|\n");
md.push_str(&format!("| Peg Target | {:.d$} |\n", self.peg_target));
md.push_str(&format!("| Book | {} |\n", self.source.label()));
md.push_str(&format!(
"| Best Bid | {} |\n",
bid_ask_or_dash(self.best_bid)
));
md.push_str(&format!(
"| Best Ask | {} |\n",
bid_ask_or_dash(self.best_ask)
));
md.push_str(&format!(
"| Mid Price | {} |\n",
price_or_dash(self.mid_price)
));
md.push_str(&format!("| Spread | {} |\n", price_or_dash(self.spread)));
if let Some(v) = self.volume_24h {
md.push_str(&format!("| Volume (24h) | {v:.0} {q} |\n"));
}
md.push_str(&format!(
"| 10k Buy Slippage | {} |\n",
exec_or_dash(&self.execution_10k_buy)
));
md.push_str(&format!(
"| 10k Sell Slippage | {} |\n",
exec_or_dash(&self.execution_10k_sell)
));
md.push_str(&format!("| Bid Depth | {:.0} |\n", self.bid_depth));
md.push_str(&format!("| Ask Depth | {:.0} |\n", self.ask_depth));
md.push_str(&format!(
"| Healthy | {} |\n",
if self.healthy { "✓" } else { "✗" }
));
if !self.checks.is_empty() {
md.push_str("\n**Health Checks:**\n");
for check in &self.checks {
md.push_str(&format!("- {} {}\n", check.icon(), check.message()));
}
}
if let Some(found) = self
.bot_patterns
.as_ref()
.filter(|b| !b.findings.is_empty())
{
md.push_str("\n**Bot Patterns** (likely market-making bots):\n");
for line in found.evidence() {
md.push_str(&format!("- {line}\n"));
}
}
md
}
}
#[cfg(test)]
mod tests {
use super::*;
fn side(start: f64, step: f64, qtys: &[f64]) -> Vec<OrderBookLevel> {
qtys.iter()
.enumerate()
.map(|(i, &quantity)| OrderBookLevel {
price: start + step * i as f64,
quantity,
})
.collect()
}
fn healthy_book() -> OrderBook {
OrderBook {
pair: "USDC/USDT".to_string(),
source: crate::market::types::BookSource::Exchange,
bids: side(0.9999, -0.0001, &[400.0; 10]),
asks: side(1.0001, 0.0001, &[400.0; 10]),
}
}
fn summarize(book: &OrderBook) -> MarketSummary {
MarketSummary::from_order_book(book, &HealthThresholds::default(), None)
}
fn failed(summary: &MarketSummary) -> Vec<String> {
summary
.checks
.iter()
.filter_map(|c| match c {
HealthCheck::Fail(m) => Some(m.clone()),
HealthCheck::Pass(_) | HealthCheck::NotApplicable(_) | HealthCheck::Warn(_) => None,
})
.collect()
}
fn deep_book() -> OrderBook {
let mut b = healthy_book();
b.bids.push(OrderBookLevel {
price: 0.97,
quantity: 100.0,
});
b.asks.push(OrderBookLevel {
price: 1.03,
quantity: 100.0,
});
b
}
fn deep_window_depth() -> f64 {
let b = healthy_book();
b.bids
.iter()
.chain(&b.asks)
.map(OrderBookLevel::value)
.sum()
}
fn with_volume(book: &OrderBook, volume: Option<f64>) -> MarketSummary {
MarketSummary::from_order_book(book, &HealthThresholds::default(), volume)
}
fn depth_volume(s: &MarketSummary) -> &HealthCheck {
let found = status_of(s, "Depth/volume");
assert_eq!(found.len(), 1, "{:?}", s.checks);
found[0]
}
#[test]
fn test_depth_volume_below_floor_fails() {
let s = with_volume(&deep_book(), Some(deep_window_depth() / 0.04));
assert!(
matches!(depth_volume(&s), HealthCheck::Fail(_)),
"{:?}",
depth_volume(&s)
);
assert!(!s.healthy);
}
#[test]
fn test_depth_volume_at_floor_passes() {
let s = with_volume(&deep_book(), Some(deep_window_depth() / 0.05));
assert!(
matches!(depth_volume(&s), HealthCheck::Pass(_)),
"{:?}",
depth_volume(&s)
);
assert!(s.healthy, "{:?}", s.checks);
}
#[test]
fn test_depth_volume_above_ceiling_warns_but_stays_healthy() {
let s = with_volume(&deep_book(), Some(deep_window_depth() / 0.12));
let c = depth_volume(&s);
assert!(matches!(c, HealthCheck::Warn(_)), "{c:?}");
assert!(c.message().contains("12.0%"), "{c:?}");
assert!(s.healthy, "a warning must not make the book unhealthy");
}
#[test]
fn test_depth_volume_without_volume_is_not_applicable() {
let s = with_volume(&deep_book(), None);
assert!(matches!(depth_volume(&s), HealthCheck::NotApplicable(_)));
let s = with_volume(&deep_book(), Some(0.0));
assert!(matches!(depth_volume(&s), HealthCheck::NotApplicable(_)));
}
#[test]
fn test_truncated_book_below_floor_is_not_applicable() {
let s = with_volume(&healthy_book(), Some(deep_window_depth() / 0.01));
let c = depth_volume(&s);
assert!(matches!(c, HealthCheck::NotApplicable(_)), "{c:?}");
assert!(c.message().contains('≥'), "{c:?}");
}
#[test]
fn test_truncated_book_above_ceiling_still_warns() {
let s = with_volume(&healthy_book(), Some(deep_window_depth() / 0.30));
let c = depth_volume(&s);
assert!(matches!(c, HealthCheck::Warn(_)), "{c:?}");
assert!(c.message().contains("≥30.0%"), "{c:?}");
}
#[test]
fn test_depth_volume_thresholds_are_overridable() {
let t = HealthThresholds {
min_depth_volume_pct: 1.0,
max_depth_volume_pct: 50.0,
..HealthThresholds::default()
};
let s = MarketSummary::from_order_book(&deep_book(), &t, Some(deep_window_depth() / 0.12));
assert!(matches!(depth_volume(&s), HealthCheck::Pass(_)));
}
#[test]
fn test_passing_level_and_depth_checks_are_shown_with_limits() {
let s = summarize(&healthy_book());
for prefix in [
"Bid levels",
"Ask levels",
"Bid depth",
"Ask depth",
"Bid top-3 depth",
"Ask top-10 depth",
] {
let found = status_of(&s, prefix);
assert_eq!(found.len(), 1, "{prefix}: {:?}", s.checks);
assert!(
matches!(found[0], HealthCheck::Pass(_)),
"{prefix}: {:?}",
found[0]
);
assert!(
found[0].message().contains("(min "),
"{prefix}: {:?}",
found[0]
);
}
}
fn amm_summary(base_reserve: f64) -> MarketSummary {
let pair = crate::chains::DexPair {
dex_name: "Uniswap V2".into(),
pair_address: "0xpool".into(),
base_token: "USDC".into(),
quote_token: "USDT".into(),
price_usd: 1.0,
volume_24h: 0.0,
liquidity_usd: 2.0 * base_reserve,
liquidity_base: Some(base_reserve),
price_change_24h: 0.0,
buys_24h: 0,
sells_24h: 0,
buys_6h: 0,
sells_6h: 0,
buys_1h: 0,
sells_1h: 0,
pair_created_at: None,
url: None,
};
let t = HealthThresholds::default();
let book =
super::super::analytics::order_book_from_analytics("ethereum", &pair, "USDC", &t);
MarketSummary::from_order_book(&book, &t, None)
}
fn status_of<'a>(s: &'a MarketSummary, prefix: &str) -> Vec<&'a HealthCheck> {
s.checks
.iter()
.filter(|c| c.message().starts_with(prefix))
.collect()
}
#[test]
fn test_amm_levels_and_spread_are_not_applicable_and_shown() {
let s = amm_summary(10_000_000.0);
assert_eq!(s.source, BookSource::SyntheticAmm);
let spread = status_of(&s, "Spread");
assert_eq!(spread.len(), 1);
assert!(matches!(spread[0], HealthCheck::NotApplicable(_)));
for side in ["Bid levels", "Ask levels"] {
let c = status_of(&s, side);
assert_eq!(c.len(), 1, "{} must always be shown", side);
assert!(matches!(c[0], HealthCheck::NotApplicable(_)), "{}", side);
}
}
#[test]
fn test_deep_amm_pool_can_be_healthy() {
let s = amm_summary(10_000_000.0);
assert!(s.healthy, "{:?}", failed(&s));
}
#[test]
fn test_shallow_amm_pool_fails_the_depth_rules() {
let s = amm_summary(1_000.0);
assert!(!s.healthy);
let f = failed(&s);
assert!(f.iter().any(|m| m.starts_with("Bid depth")), "{:?}", f);
assert!(
f.iter().any(|m| m.starts_with("Ask top-3 depth")),
"{:?}",
f
);
assert!(
f.iter().any(|m| m.starts_with("Bid top-10 depth")),
"{:?}",
f
);
}
#[test]
fn test_exchange_book_rules_are_unchanged() {
let s = summarize(&healthy_book());
assert_eq!(s.source, BookSource::Exchange);
assert!(
s.checks
.iter()
.filter(|c| !c.message().starts_with("Depth/volume"))
.all(|c| !matches!(c, HealthCheck::NotApplicable(_))),
"{:?}",
s.checks
);
}
#[test]
fn test_book_source_labels_and_json_names() {
for (src, json, label) in [
(BookSource::Exchange, "exchange", "exchange order book"),
(
BookSource::SyntheticAmm,
"synthetic_amm",
"synthetic (AMM curve x·y=k)",
),
(
BookSource::SyntheticEstimate,
"synthetic_estimate",
"synthetic estimate (pool reserves not reported)",
),
] {
assert_eq!(serde_json::to_value(src).unwrap(), serde_json::json!(json));
assert_eq!(src.label(), label);
}
assert_eq!(BookSource::default(), BookSource::Exchange);
let na = HealthCheck::NotApplicable("x".into());
assert_eq!((na.icon(), na.status(), na.message()), ("–", "n/a", "x"));
}
#[test]
fn test_check_json_shape_includes_na() {
let v = serde_json::to_value(HealthCheck::NotApplicable("x".into())).unwrap();
assert_eq!(v, serde_json::json!({"status": "n/a", "message": "x"}));
let v = serde_json::to_value(HealthCheck::Fail("y".into())).unwrap();
assert_eq!(v, serde_json::json!({"status": "fail", "message": "y"}));
}
#[test]
fn test_to_json_shape_is_the_one_the_web_and_cli_share() {
let s = MarketSummary::from_order_book(
&healthy_book(),
&HealthThresholds::default(),
Some(50_000.0),
);
let v = s.to_json();
for key in [
"pair",
"book_source",
"peg_target",
"best_bid",
"best_ask",
"mid_price",
"spread",
"volume_24h",
"bid_depth",
"ask_depth",
"bid_outliers",
"ask_outliers",
"bid_levels",
"ask_levels",
"healthy",
"slippage_warn_bps",
"execution_10k_buy",
"execution_10k_sell",
"bids",
"asks",
"checks",
] {
assert!(v.get(key).is_some(), "missing key {key}");
}
assert_eq!(v["pair"], "USDC/USDT");
assert_eq!(v["book_source"], "exchange");
assert_eq!(v["slippage_warn_bps"], SLIPPAGE_WARN_BPS);
assert_eq!(v["bid_levels"], 10);
let buy = &v["execution_10k_buy"];
for key in ["notional_usdt", "vwap", "slippage_bps", "fillable"] {
assert!(buy.get(key).is_some(), "missing exec key {key}");
}
let level = &v["bids"][0];
for key in ["price", "quantity", "value"] {
assert!(level.get(key).is_some(), "missing level key {key}");
}
}
#[test]
fn test_to_json_caps_levels_at_twenty_but_counts_all() {
let mut book = healthy_book();
book.bids = side(0.9999, -0.0001, &[400.0; 25]);
let s = summarize(&book);
let v = s.to_json();
assert_eq!(v["bids"].as_array().unwrap().len(), 20);
assert_eq!(v["bid_levels"], 25);
}
#[test]
fn test_format_markdown_table_with_deviations_and_checks() {
let s = MarketSummary::from_order_book(
&healthy_book(),
&HealthThresholds::default(),
Some(50_000.0),
);
let md = s.format_markdown(Some("binance"));
assert!(md.contains("**Venue:** binance"), "{md}");
assert!(md.contains("| Peg Target | 1.000000 |"), "{md}");
assert!(md.contains("0.999900 (-0.010%)"), "{md}");
assert!(md.contains("1.000100 (+0.010%)"), "{md}");
assert!(md.contains("| Mid Price | 1.000000 |"), "{md}");
assert!(md.contains("| Volume (24h) | 50000 USDT |"), "{md}");
assert!(md.contains("| Healthy | ✓ |"), "{md}");
assert!(md.contains("**Health Checks:**"), "{md}");
assert!(md.contains("- ✓ No sells below peg"), "{md}");
let md = s.format_markdown(None);
assert!(!md.contains("**Venue:**"), "{md}");
}
#[test]
fn test_format_markdown_dashes_and_unhealthy_icon() {
let mut s = summarize(&healthy_book());
s.best_bid = None;
s.best_ask = None;
s.mid_price = None;
s.spread = None;
s.volume_24h = None;
s.healthy = false;
s.checks = vec![];
let md = s.format_markdown(None);
assert!(md.contains("| Best Bid | - |"), "{md}");
assert!(!md.contains("Volume (24h)"), "{md}");
assert!(md.contains("| Healthy | ✗ |"), "{md}");
assert!(!md.contains("Health Checks:"), "{md}");
}
#[test]
fn test_text_report_names_the_book_source() {
let _lock = crate::display::terminal::width_lock();
let s = amm_summary(10_000_000.0);
let text = s.format_text(Some("eth"));
assert!(text.contains("synthetic (AMM curve x·y=k)"), "{}", text);
assert!(text.contains("– "), "n/a rows use the dash icon: {}", text);
}
#[test]
fn test_market_summary_from_order_book() {
let summary = summarize(&healthy_book());
assert!(summary.healthy, "{:?}", failed(&summary));
assert_eq!(summary.bids.len(), 10);
assert_eq!(summary.asks.len(), 10);
assert!(summary.bid_depth > 3000.0);
assert!(summary.ask_depth > 3000.0);
}
#[test]
fn test_peg_comes_from_thresholds() {
let thresholds = HealthThresholds {
peg_target: 1.01,
peg_range: 0.01,
..HealthThresholds::default()
};
let summary = MarketSummary::from_order_book(&healthy_book(), &thresholds, None);
assert_eq!(summary.peg_target, 1.01);
assert!(
failed(&summary).iter().any(|m| m.contains("below peg")),
"{:?}",
failed(&summary)
);
}
#[test]
fn test_default_criteria_values() {
let t = HealthThresholds::default();
assert_eq!(t.min_levels, 10);
assert_eq!(t.max_spread_pct, 3.0);
assert_eq!(t.min_top3_depth, 300.0);
assert_eq!(t.min_top10_depth, 2000.0);
assert_eq!(t.min_depth, 3000.0);
assert_eq!(t.depth_window_pct, 2.0);
assert_eq!(t.min_depth_volume_pct, 5.0);
assert_eq!(t.max_depth_volume_pct, 10.0);
}
#[test]
fn test_nine_levels_is_unhealthy() {
let mut book = healthy_book();
book.bids = side(0.9999, -0.0001, &[1000.0; 9]);
let summary = summarize(&book);
assert!(!summary.healthy);
assert_eq!(failed(&summary), vec!["Bid levels: 9 < 10 minimum"]);
}
#[test]
fn test_zero_quantity_level_is_not_valid() {
let mut book = healthy_book();
book.asks[9].quantity = 0.0;
book.asks.push(OrderBookLevel {
price: 1.0011,
quantity: 0.0,
});
let summary = summarize(&book);
assert!(
failed(&summary).contains(&"Ask levels: 9 < 10 minimum".to_string()),
"{:?}",
failed(&summary)
);
}
#[test]
fn test_spread_over_three_percent_is_unhealthy() {
let mut book = healthy_book();
book.asks = side(1.04, 0.0001, &[400.0; 10]);
let summary = summarize(&book);
assert!(!summary.healthy);
assert!(
failed(&summary).iter().any(|m| m.starts_with("Spread:")),
"{:?}",
failed(&summary)
);
}
#[test]
fn test_spread_under_three_percent_passes_spread_check() {
let mut book = healthy_book();
book.asks = side(1.02, 0.0001, &[400.0; 10]);
let summary = summarize(&book);
assert!(
!failed(&summary).iter().any(|m| m.starts_with("Spread:")),
"{:?}",
failed(&summary)
);
}
#[test]
fn test_thin_top3_is_unhealthy_even_with_deep_book() {
let mut book = healthy_book();
let mut qtys = vec![50.0; 3];
qtys.extend([500.0; 7]);
book.bids = side(0.9999, -0.0001, &qtys);
let summary = summarize(&book);
assert_eq!(
failed(&summary),
vec!["Bid top-3 depth: 150 USDT < 300 USDT minimum"]
);
}
#[test]
fn test_top10_counts_only_first_ten_levels() {
let mut book = healthy_book();
let mut qtys = vec![150.0; 10];
qtys.push(2000.0);
book.asks = side(1.0001, 0.0001, &qtys);
let summary = summarize(&book);
assert!(summary.ask_depth >= 3000.0);
let fails = failed(&summary);
assert_eq!(fails.len(), 1, "{fails:?}");
assert!(fails[0].starts_with("Ask top-10 depth:"), "{fails:?}");
}
#[test]
fn test_format_text_with_chain() {
let book = OrderBook {
pair: "USDC/USDT".to_string(),
source: crate::market::types::BookSource::Exchange,
bids: vec![OrderBookLevel {
price: 1.0,
quantity: 100.0,
}],
asks: vec![OrderBookLevel {
price: 1.0,
quantity: 100.0,
}],
};
let summary = MarketSummary::from_order_book(&book, &HealthThresholds::default(), None);
let out = summary.format_text(Some("biconomy"));
assert!(out.contains("biconomy"));
assert!(out.contains("Venue"));
}
#[test]
fn test_format_text_without_chain() {
let book = OrderBook {
pair: "X/Y".to_string(),
source: crate::market::types::BookSource::Exchange,
bids: vec![OrderBookLevel {
price: 1.0,
quantity: 10.0,
}],
asks: vec![],
};
let summary = MarketSummary::from_order_book(&book, &HealthThresholds::default(), None);
let out = summary.format_text(None);
assert!(out.contains("X/Y"));
assert!(!out.contains("Venue"));
}
#[test]
fn test_health_check_sells_below_peg() {
let book = OrderBook {
pair: "USDC/USDT".to_string(),
source: crate::market::types::BookSource::Exchange,
bids: vec![OrderBookLevel {
price: 0.9995,
quantity: 1000.0,
}],
asks: vec![
OrderBookLevel {
price: 0.9990,
quantity: 100.0,
}, OrderBookLevel {
price: 1.0001,
quantity: 500.0,
},
],
};
let summary = MarketSummary::from_order_book(&book, &HealthThresholds::default(), None);
assert!(!summary.healthy);
let has_fail = summary
.checks
.iter()
.any(|c| matches!(c, HealthCheck::Fail(m) if m.contains("sell")));
assert!(has_fail);
}
#[test]
fn test_token_lines_sit_under_the_header() {
let book = OrderBook {
pair: "USDN/USDT".to_string(),
source: BookSource::Exchange,
bids: vec![OrderBookLevel {
price: 0.9999,
quantity: 1000.0,
}],
asks: vec![OrderBookLevel {
price: 1.0001,
quantity: 1000.0,
}],
};
let summary = MarketSummary::from_order_book(&book, &HealthThresholds::default(), None);
let tokens = [
super::super::assets::TokenAddress {
symbol: "USDN".into(),
network: "ETH".into(),
contract: Some("0xeC747DAf41A4Ca4ebE72356592F8301924132907".into()),
other_networks: 0,
},
super::super::assets::TokenAddress {
symbol: "USDT".into(),
network: "ETH".into(),
contract: Some("0xdAC17F958D2ee523a2206206994597C13D831ec7".into()),
other_networks: 7,
},
];
let out = summary.format_text_with_tokens(Some("weex"), &tokens);
let at = |needle: &str| {
out.lines()
.position(|l| l.contains(needle))
.unwrap_or_else(|| panic!("no {needle:?} in {out}"))
};
let header = at("USDN/USDT (weex)");
let base = at("USDN (ETH) 0xeC747DAf41A4Ca4ebE72356592F8301924132907");
let quote =
at("USDT (ETH) 0xdAC17F958D2ee523a2206206994597C13D831ec7 (+7 networks)");
assert_eq!((base, quote), (header + 1, header + 2), "{out}");
assert!(at("Metrics") > quote, "{out}");
assert_eq!(
summary.format_text(Some("weex")),
summary.format_text_with_tokens(Some("weex"), &[])
);
}
#[test]
fn test_format_text_with_volume_and_spread() {
let book = OrderBook {
pair: "USDC/USDT".to_string(),
source: crate::market::types::BookSource::Exchange,
bids: vec![
OrderBookLevel {
price: 0.9999,
quantity: 1000.0,
},
OrderBookLevel {
price: 0.9998,
quantity: 1000.0,
},
],
asks: vec![
OrderBookLevel {
price: 1.0001,
quantity: 1000.0,
},
OrderBookLevel {
price: 1.0002,
quantity: 1000.0,
},
],
};
let summary =
MarketSummary::from_order_book(&book, &HealthThresholds::default(), Some(50_000.0));
let out = summary.format_text(Some("binance"));
let volume = out.lines().find(|l| l.contains("Volume (24h)")).unwrap();
assert!(volume.ends_with("50.00K USDT"), "{volume}");
assert!(out.contains("Ask Side — 2 levels, 2.00K USDT"), "{out}");
assert!(out.contains("Spread"));
}
#[test]
fn test_format_text_with_outliers_and_many_levels() {
let mut asks: Vec<OrderBookLevel> = (0..12)
.map(|i| OrderBookLevel {
price: 1.0 + 0.0001 * (i + 1) as f64, quantity: 500.0,
})
.collect();
asks.push(OrderBookLevel {
price: 1.01, quantity: 100.0,
});
let book = OrderBook {
pair: "USDC/USDT".to_string(),
source: crate::market::types::BookSource::Exchange,
bids: vec![
OrderBookLevel {
price: 0.9999,
quantity: 600.0,
},
OrderBookLevel {
price: 0.9998,
quantity: 600.0,
},
OrderBookLevel {
price: 0.9997,
quantity: 600.0,
},
OrderBookLevel {
price: 0.9996,
quantity: 600.0,
},
OrderBookLevel {
price: 0.9995,
quantity: 600.0,
},
OrderBookLevel {
price: 0.9994,
quantity: 600.0,
},
],
asks,
};
let summary = MarketSummary::from_order_book(&book, &HealthThresholds::default(), None);
let out = summary.format_text(None);
assert!(
out.contains("outliers excl.") || summary.ask_outliers > 0 || summary.bid_outliers > 0
);
assert!(out.contains("... +")); }
#[test]
fn test_format_text_execution_fillable() {
let book = OrderBook {
pair: "USDC/USDT".to_string(),
source: crate::market::types::BookSource::Exchange,
bids: vec![OrderBookLevel {
price: 0.9999,
quantity: 20_000.0,
}],
asks: vec![OrderBookLevel {
price: 1.0001,
quantity: 20_000.0,
}],
};
let summary = MarketSummary::from_order_book(&book, &HealthThresholds::default(), None);
let out = summary.format_text(Some("cex"));
assert!(out.contains("Exec 10K buy"));
assert!(out.contains("Exec 10K sell"));
assert!(out.contains("slippage") || out.contains("insufficient"));
}
#[test]
fn test_format_text_bid_outliers_and_many_bid_levels() {
let mut bids: Vec<OrderBookLevel> = (0..12)
.map(|i| OrderBookLevel {
price: 1.0 - 0.0001 * (i + 1) as f64,
quantity: 500.0,
})
.collect();
bids.push(OrderBookLevel {
price: 0.98, quantity: 100.0,
});
let book = OrderBook {
pair: "USDC/USDT".to_string(),
source: crate::market::types::BookSource::Exchange,
bids,
asks: vec![
OrderBookLevel {
price: 1.0001,
quantity: 600.0,
},
OrderBookLevel {
price: 1.0002,
quantity: 600.0,
},
],
};
let summary = MarketSummary::from_order_book(&book, &HealthThresholds::default(), None);
let out = summary.format_text(None);
assert!(summary.bid_outliers > 0);
assert!(out.contains("outliers excl."));
assert!(out.contains("... +"));
}
#[test]
fn test_top_depth_never_exceeds_in_band_depth_with_far_outliers() {
let mut bids = side(0.9999, 0.0, &[100.0]);
bids.extend(side(0.5, 0.0, &[100_000.0]));
let b = OrderBook {
bids,
..healthy_book()
};
let s = summarize(&b);
assert_eq!(s.top_depth.bid_top3, s.bid_depth);
assert!(s.top_depth.bid_top10 <= s.bid_depth);
assert!((s.bid_depth - 99.99).abs() < 1e-9);
assert!((b.total_depth(BookSide::Bid) - 50_099.99).abs() < 1e-9);
assert_eq!(s.bid_outliers, 1);
}
#[test]
fn test_shown_level_count_equals_the_checked_one() {
let mut asks = side(1.0001, 0.0001, &[400.0; 8]);
asks.extend(side(1.01, 0.001, &[400.0; 3]));
asks.push(OrderBookLevel {
price: 1.0002,
quantity: 0.0,
});
let s = summarize(&OrderBook {
asks,
..healthy_book()
});
assert_eq!(s.ask_levels(), 8);
assert_eq!(s.ask_outliers, 3);
assert!(failed(&s).contains(&"Ask levels: 8 < 10 minimum".to_string()));
assert!(s.format_text(None).contains("Ask Side — 8 levels"));
}
#[test]
fn test_a_zero_quantity_level_is_not_a_sell_below_peg() {
let mut asks = healthy_book().asks;
asks.insert(
0,
OrderBookLevel {
price: 0.9999,
quantity: 0.0,
},
);
let s = summarize(&OrderBook {
asks,
..healthy_book()
});
assert!(!failed(&s).iter().any(|m| m.contains("below peg")));
}
#[test]
fn test_peg_status_follows_the_mid_and_the_health_checks() {
let s = summarize(&healthy_book());
assert_eq!(s.peg_status(), PegStatus::OnPeg);
let mut unhealthy = s.clone();
unhealthy.healthy = false;
assert_eq!(unhealthy.peg_status(), PegStatus::BookUnhealthy);
let mut off = s.clone();
off.mid_price = Some(0.9);
assert_eq!(off.peg_status(), PegStatus::Deviation);
off.mid_price = None;
assert_eq!(off.peg_status(), PegStatus::NoPrice);
}
#[test]
fn test_peg_bands_come_from_the_thresholds() {
let bands = HealthThresholds {
peg_target: 0.5,
peg_range: 0.002,
..HealthThresholds::default()
}
.peg_bands();
assert_eq!(bands.classify(0.5015), PegBand::OnPeg);
assert_eq!(bands.classify(0.505), PegBand::Slight);
assert_eq!(bands.classify(0.52), PegBand::Off);
assert_eq!(bands.outlier_band(), (0.49, 0.51));
}
#[test]
fn test_slight_deviation_is_between_the_peg_range_and_the_outlier_band() {
let mut s = summarize(&healthy_book());
s.mid_price = Some(1.003);
assert_eq!(s.peg_status(), PegStatus::SlightDeviation);
}
#[test]
fn test_peg_band_edges_are_inclusive_like_the_outlier_filter() {
let bands = PegBands::new(1.0, 0.25);
assert_eq!(bands.classify(1.25), PegBand::OnPeg);
assert_eq!(bands.classify(2.25), PegBand::Slight);
assert_eq!(bands.classify(2.26), PegBand::Off);
let book = OrderBook {
asks: vec![OrderBookLevel {
price: 2.25,
quantity: 1.0,
}],
..healthy_book()
};
assert_eq!(
book.valid_levels_in_band(BookSide::Ask, bands.eligible_band(BookSide::Ask))
.count(),
1
);
}
#[test]
fn test_text_report_notes_that_best_prices_include_outlier_levels() {
let mut asks = healthy_book().asks;
asks.push(OrderBookLevel {
price: 1.5,
quantity: 10.0,
});
let with_outlier = summarize(&OrderBook {
asks,
..healthy_book()
});
assert!(
with_outlier
.format_text(None)
.contains("best prices include")
);
assert!(
!summarize(&healthy_book())
.format_text(None)
.contains("best prices include")
);
}
#[test]
fn test_depth_cycles_rewrite_the_bot_pattern_check() {
let book = OrderBook {
pair: "USDN/USDT".to_string(),
source: crate::market::types::BookSource::Exchange,
bids: side(0.9999, -0.0001, &[100.0; 20]),
asks: side(1.0001, 0.0001, &[100.0; 20]),
};
let mut summary = MarketSummary::from_order_book(&book, &HealthThresholds::default(), None);
summary.check_bot_patterns(Err("no tape"));
let checks = summary.checks.len();
let flat: Vec<(i64, f64)> = (0..120).map(|i| (i * 30_000, 3_300.0)).collect();
let saw: Vec<(i64, f64)> = (0..120)
.map(|i| (i * 30_000, 8_000.0 * 0.95f64.powi((i % 29) as i32)))
.collect();
summary.check_depth_cycles(&flat, &saw);
summary.check_depth_cycles(&flat, &saw);
assert_eq!(summary.checks.len(), checks);
let bot: Vec<&HealthCheck> = summary
.checks
.iter()
.filter(|c| format!("{c:?}").contains("ot patterns"))
.collect();
assert_eq!(bot.len(), 1, "{:?}", summary.checks);
let HealthCheck::Warn(m) = bot[0] else {
panic!("{bot:?}")
};
assert!(m.contains("ask depth cycle"), "{m}");
}
#[test]
fn test_markdown_lists_the_bot_patterns_one_a_line() {
let book = OrderBook {
pair: "USDC/USDT".to_string(),
source: crate::market::types::BookSource::Exchange,
bids: side(0.9999, -0.0001, &[100.0; 20]),
asks: side(1.0001, 0.0001, &[100.0; 20]),
};
let mut summary = MarketSummary::from_order_book(&book, &HealthThresholds::default(), None);
let tape: Vec<Trade> = (0..60u64)
.map(|i| Trade {
price: 1.0,
quantity: 1_000.0,
quote_quantity: None,
timestamp_ms: i * 2_000,
side: crate::market::types::TradeSide::Buy,
id: None,
})
.collect();
summary.check_bot_patterns(Ok(&tape));
let md = summary.format_markdown(Some("binance"));
assert!(
md.contains("**Bot Patterns** (likely market-making bots):"),
"{md}"
);
assert!(
md.contains("- Repeated size: 100% of 60 trades are exactly 1000"),
"{md}"
);
assert!(md.contains("- Bid ladder: 20 levels × 100"), "{md}");
}
}