simulator-api 0.24.0

Wire-protocol types for the Solana simulator backtest WebSocket API
Documentation
//! The report as a table and as JSON.

use super::flow::{Direction, Flow, Report, fold};
use crate::mints::short_mint;

/// A longtail book reaches the hundreds, and the tail is individually negligible but
/// collectively not, so the rest fold into one line rather than being dropped.
const MAX_DIRECTIONS: usize = 12;

impl Report {
    /// The gap between the columns includes the reroute effect, not just any configured change.
    ///
    /// `subtitle` prints beside the venue name — the slot range, say, which this module cannot
    /// know.
    pub fn render(&self, subtitle: Option<&str>) -> String {
        let target = self.target();
        let mut out = String::new();
        let name = target
            .label()
            .map(str::to_string)
            .or_else(|| target.program().map(|program| program.to_string()))
            .unwrap_or_else(|| "venue".to_string());
        out.push_str(&format!("\n  {name}"));
        if let Some(subtitle) = subtitle {
            out.push_str(&format!("   {subtitle}"));
        }
        out.push_str("\n\n");

        let priced = self.sol_usd().is_some();
        out.push_str(&row("", "on L1", "re-quoted", "delta"));
        let total = self.total();
        out.push_str(&rows_for(&total, "", priced));
        out.push_str(&share_row(&total, ""));

        let mut ranked: Vec<_> = self
            .by_direction()
            .filter(|(_, direction)| direction.present())
            .collect();
        ranked.sort_unstable_by_key(|(mints, direction)| {
            (std::cmp::Reverse(direction.rank()), *mints)
        });
        for ((input, output), direction) in ranked.iter().take(MAX_DIRECTIONS) {
            out.push_str(&format!(
                "\n  {} → {}\n",
                short_mint(input),
                short_mint(output)
            ));
            out.push_str(&rows_for(direction, "  ", priced));
            out.push_str(&share_row(direction, "  "));
        }
        if let Some(rest) = ranked.get(MAX_DIRECTIONS..).filter(|rest| !rest.is_empty()) {
            let tail = fold(rest.iter().map(|(_, direction)| **direction));
            out.push_str(&format!("\n  + {} more directions\n", rest.len()));
            out.push_str(&rows_for(&tail, "  ", priced));
            out.push_str(&share_row(&tail, "  "));
        }

        out
    }

    /// Kept flat so several arms join as rows.
    pub fn to_json(&self) -> serde_json::Value {
        let target = self.target();
        let total = self.total();
        let directions: Vec<_> = self
            .by_direction()
            .map(|((input, output), direction)| {
                serde_json::json!({
                    "inputMint": input,
                    "outputMint": output,
                    "l1": flow_json(&direction.l1),
                    "requoted": flow_json(&direction.requoted),
                    "sharePct": direction.share().map(|(l1, _)| l1),
                    "requotedSharePct": direction.share().map(|(_, requoted)| requoted),
                })
            })
            .collect();
        serde_json::json!({
            "label": target.label(),
            "program": target.program().map(|program| program.to_string()),
            "total": {
                "l1": flow_json(&total.l1),
                "requoted": flow_json(&total.requoted),
            },
            // Per-leg normalized, unlike the per-hop `usd` above.
            "share": {
                "l1Pct": total.share().map(|(l1, _)| l1),
                "requotedPct": total.share().map(|(_, requoted)| requoted),
            },
            "byDirection": directions,
        })
    }
}

fn flow_json(flow: &Flow) -> serde_json::Value {
    serde_json::json!({
        "swaps": flow.legs,
        "usd": flow.usd,
        "swapsWithoutAmount": flow.without_amount,
        "swapsWithoutPrice": flow.without_price,
    })
}

fn share_row(direction: &Direction, indent: &str) -> String {
    let Some((l1, requoted)) = direction.share() else {
        return String::new();
    };
    row(
        &format!("{indent}share"),
        &format!("{l1:.1}%"),
        &format!("{requoted:.1}%"),
        &format!("{:+.1}pp", requoted - l1),
    )
}

fn row(label: &str, l1: &str, requoted: &str, delta: &str) -> String {
    format!("  {label:<10} {l1:>14} {requoted:>14} {delta:>14}\n")
}

/// Legs and dollars only: each direction's amount is in its own input mint, and dollars are the
/// only figure that crosses them.
fn rows_for(direction: &Direction, indent: &str, priced: bool) -> String {
    let mut out = totals("swaps", indent, direction.l1.legs, direction.requoted.legs);
    if priced && !direction.fully_unpriced() {
        out.push_str(&usd_row(direction, indent));
    }
    if direction.unpriced() {
        out.push_str(&format!(
            "{indent}  ({} swaps in mints this range could not price)\n",
            direction.l1.without_price + direction.requoted.without_price
        ));
    }
    out
}

fn totals(label: &str, indent: &str, l1: u64, requoted: u64) -> String {
    row(
        &format!("{indent}{label}"),
        &l1.to_string(),
        &requoted.to_string(),
        &signed(i128::from(requoted) - i128::from(l1)),
    )
}

fn usd_row(direction: &Direction, indent: &str) -> String {
    row(
        &format!("{indent}volume"),
        &money(direction.l1.usd),
        &money(direction.requoted.usd),
        &format!(
            "{}{}",
            if direction.requoted.usd < direction.l1.usd {
                "-"
            } else {
                "+"
            },
            money((direction.requoted.usd - direction.l1.usd).abs())
        ),
    )
}

fn money(usd: f64) -> String {
    match usd {
        usd if usd >= 1e6 => format!("${:.2}M", usd / 1e6),
        usd if usd >= 1e3 => format!("${:.1}k", usd / 1e3),
        usd => format!("${usd:.0}"),
    }
}

fn signed(delta: i128) -> String {
    match delta < 0 {
        true => delta.to_string(),
        false => format!("+{delta}"),
    }
}