1use std::{
19 cmp::Ordering,
20 collections::{BTreeMap, HashMap, HashSet, hash_map::Entry},
21};
22
23use serde::Serialize;
24use solana_address::Address;
25
26use super::{
27 fmt::{commas_u, median},
28 prices::Prices,
29 record::{LegRecord, SwapMode},
30};
31use crate::{mints, route_plan::RoutePlan};
32
33pub const VENUE_DUST_USD: f64 = 1e4;
35pub const OUTLIER_BPS: f64 = 2000.0;
37
38#[derive(Default, Debug)]
39pub struct LoadStats {
40 pub dropped_multi_row: usize,
41}
42
43pub fn collect(rows: Vec<LegRecord>) -> (Vec<LegRecord>, LoadStats) {
47 let mut kept: Vec<LegRecord> = Vec::new();
48 let mut first_row: HashMap<String, usize> = HashMap::new();
49 let mut dropped: HashSet<usize> = HashSet::new();
50 for row in rows {
51 if row.swap_mode == SwapMode::ExactOut && !mints::is_major(&row.input_mint) {
52 continue;
53 }
54 match first_row.entry(row.original_signature.clone()) {
55 Entry::Vacant(slot) => {
56 slot.insert(kept.len());
57 kept.push(row);
58 }
59 Entry::Occupied(slot) => {
60 dropped.insert(*slot.get());
61 }
62 }
63 }
64 let stats = LoadStats {
65 dropped_multi_row: dropped.len(),
66 };
67 (
68 kept.into_iter()
69 .enumerate()
70 .filter(|(index, _)| !dropped.contains(index))
71 .map(|(_, row)| row)
72 .collect(),
73 stats,
74 )
75}
76
77fn plan_input(plan: &RoutePlan, mint: &Address) -> Option<i64> {
81 let mint = mint.to_string();
82 let mut total = 0i64;
83 for hop in plan.hops() {
84 if hop.input_mint() != Some(mint.as_str()) {
85 continue;
86 }
87 total += match hop.swap_info.in_amount.as_deref() {
90 None => 0,
91 Some(amount) => amount
92 .parse::<i64>()
93 .ok()
94 .or_else(|| amount.parse::<f64>().ok().map(|float| float as i64))?,
95 };
96 }
97 (total > 0).then_some(total)
98}
99
100fn venue_weights(plan: &RoutePlan) -> Vec<(Option<&str>, f64)> {
103 plan.hops()
104 .iter()
105 .map(|hop| (hop.label(), hop.percent as f64 / 100.0))
106 .collect()
107}
108
109pub struct Measurement {
111 pub outcome: Ordering,
112 pub l1_usd: f64,
113 pub our_usd: f64,
114}
115
116pub fn measure(record: &LegRecord, prices: &Prices) -> Option<Measurement> {
119 match record.swap_mode {
120 SwapMode::ExactOut => measure_exact_out(record, prices),
121 SwapMode::ExactIn => measure_exact_in(record, prices),
122 }
123}
124
125fn measure_exact_out(record: &LegRecord, prices: &Prices) -> Option<Measurement> {
126 let mint = &record.input_mint;
127 let ours = plan_input(record.route_plan.as_ref()?, mint)?;
128 let l1 = plan_input(record.original_route_plan.as_ref()?, mint)?;
129 let l1_usd = prices.usd(mint, l1 as f64)?;
130 let our_usd = prices.usd(mint, (2 * l1 - ours) as f64)?;
132 if l1_usd <= 0.0 || (our_usd / l1_usd - 1.0).abs() * 1e4 > OUTLIER_BPS {
133 return None;
134 }
135 Some(Measurement {
136 outcome: l1.cmp(&ours),
137 l1_usd,
138 our_usd,
139 })
140}
141
142fn measure_exact_in(record: &LegRecord, prices: &Prices) -> Option<Measurement> {
143 let ours = record.realized_output_amount.unwrap_or(0) as i64;
144 let l1 = record.original_realized_output_amount.unwrap_or(0) as i64;
145 let bps = (ours as f64 / l1 as f64 - 1.0) * 1e4;
146 let scaled = |usd: f64| usd * (1.0 + bps / 1e4);
147 let (l1_usd, our_usd) = if mints::is_major(&record.output_mint) {
150 (
151 prices.usd(&record.output_mint, l1 as f64),
152 prices.usd(&record.output_mint, ours as f64),
153 )
154 } else if mints::is_major(&record.input_mint) {
155 let spend = prices.usd(&record.input_mint, record.amount as f64);
156 (spend, spend.map(scaled))
157 } else if let Some(l1_usd) = prices.usd(&record.output_mint, l1 as f64) {
158 (Some(l1_usd), prices.usd(&record.output_mint, ours as f64))
159 } else {
160 let spend = prices.usd(&record.input_mint, record.amount as f64)?;
161 (Some(spend), Some(scaled(spend)))
162 };
163 let (l1_usd, our_usd) = (l1_usd?, our_usd?);
164 if l1_usd <= 0.0 || bps.abs() > OUTLIER_BPS {
165 return None;
166 }
167 Some(Measurement {
168 outcome: ours.cmp(&l1),
169 l1_usd,
170 our_usd,
171 })
172}
173
174#[derive(Default, Clone, Serialize)]
175pub struct SideTotals {
176 pub vol: f64,
177 pub net: f64,
178}
179
180#[derive(Default, Clone, Serialize)]
181pub struct VenueTotals {
182 pub us: f64,
183 pub l1: f64,
184 pub net: f64,
185}
186
187#[derive(Default, Clone, Serialize)]
189pub struct RangeReport {
190 pub n: u64,
191 pub better: u64,
192 pub same: u64,
193 pub worse: u64,
194 pub vol: f64,
195 pub net: f64,
196 pub vol_better: f64,
197 pub vol_same: f64,
198 pub vol_worse: f64,
199 pub bps: Vec<f64>,
200 pub majors: SideTotals,
201 pub exotic: SideTotals,
202 pub venues: BTreeMap<String, VenueTotals>,
203}
204
205impl RangeReport {
206 pub fn from_records(records: &[LegRecord], prices: &Prices) -> Self {
207 let mut report = Self::default();
208 for record in records {
209 if let Some(measurement) = measure(record, prices) {
210 report.record(record, &measurement);
211 }
212 }
213 report
214 }
215
216 fn record(&mut self, leg: &LegRecord, measurement: &Measurement) {
217 let delta = measurement.our_usd - measurement.l1_usd;
218 self.n += 1;
219 let (count, vol) = match measurement.outcome {
220 Ordering::Greater => (&mut self.better, &mut self.vol_better),
221 Ordering::Equal => (&mut self.same, &mut self.vol_same),
222 Ordering::Less => (&mut self.worse, &mut self.vol_worse),
223 };
224 *count += 1;
225 *vol += measurement.l1_usd;
226 self.vol += measurement.l1_usd;
227 self.net += delta;
228 if measurement.l1_usd > 0.0 {
229 self.bps.push(delta / measurement.l1_usd * 1e4);
230 }
231 let side = if mints::trades_only_majors(&leg.input_mint, &leg.output_mint) {
232 &mut self.majors
233 } else {
234 &mut self.exotic
235 };
236 side.vol += measurement.l1_usd;
237 side.net += delta;
238 self.attribute_venues(leg, measurement.l1_usd, delta);
239 }
240
241 fn attribute_venues(&mut self, leg: &LegRecord, l1_usd: f64, delta: f64) {
244 for (ours, plan) in [(true, &leg.route_plan), (false, &leg.original_route_plan)] {
245 let Some(plan) = plan else {
246 continue;
247 };
248 let weights = venue_weights(plan);
249 let total: f64 = weights.iter().map(|(_, weight)| weight).sum();
250 let total = if total == 0.0 { 1.0 } else { total };
251 for (label, weight) in weights {
252 let label = label.unwrap_or("(unlabeled)");
253 let entry = self.venues.entry(label.to_string()).or_default();
254 let share = weight / total;
255 if ours {
256 entry.us += l1_usd * share;
257 entry.net += delta * share;
258 } else {
259 entry.l1 += l1_usd * share;
260 }
261 }
262 }
263 }
264
265 pub fn merge(&mut self, other: &Self) {
266 self.n += other.n;
267 self.better += other.better;
268 self.same += other.same;
269 self.worse += other.worse;
270 self.vol += other.vol;
271 self.net += other.net;
272 self.vol_better += other.vol_better;
273 self.vol_same += other.vol_same;
274 self.vol_worse += other.vol_worse;
275 self.bps.extend_from_slice(&other.bps);
276 self.majors.vol += other.majors.vol;
277 self.majors.net += other.majors.net;
278 self.exotic.vol += other.exotic.vol;
279 self.exotic.net += other.exotic.net;
280 for (venue, totals) in &other.venues {
281 let entry = self.venues.entry(venue.clone()).or_default();
282 entry.us += totals.us;
283 entry.l1 += totals.l1;
284 entry.net += totals.net;
285 }
286 }
287
288 pub fn vw_bps(&self) -> f64 {
289 if self.vol == 0.0 {
290 0.0
291 } else {
292 self.net / self.vol * 1e4
293 }
294 }
295
296 pub fn render(&self, name: &str) -> String {
297 if self.vol == 0.0 {
298 return format!("### {name}: no measurable legs\n");
299 }
300 let mut lines = self.summary_lines(name);
301 lines.push(String::new());
302 lines.extend(self.venue_table());
303 lines.push(String::new());
304 lines.join("\n")
305 }
306
307 fn summary_lines(&self, name: &str) -> Vec<String> {
308 let mut bps = self.bps.clone();
309 let (q1_at, q3_at) = (bps.len() / 4, 3 * bps.len() / 4);
310 let med = median(&mut bps);
311 let mut lines = vec![
312 format!("### {name}"),
313 format!(
314 "vw vs L1: **{:+.2} bps** (net {}$ on ${})",
315 self.vw_bps(),
316 commas_signed(self.net),
317 commas(self.vol)
318 ),
319 format!(
320 "b/s/w: {} / {} / {} (by volume {:.1} / {:.1} / {:.1}%)",
321 commas_u(self.better),
322 commas_u(self.same),
323 commas_u(self.worse),
324 self.vol_better / self.vol * 100.0,
325 self.vol_same / self.vol * 100.0,
326 self.vol_worse / self.vol * 100.0
327 ),
328 format!(
329 "per-leg bps: median {med:+.2}, IQR [{:+.2}, {:+.2}]",
330 bps[q1_at], bps[q3_at]
331 ),
332 ];
333 for (label, side) in [
334 ("majors<->majors", &self.majors),
335 ("exotic-involved", &self.exotic),
336 ] {
337 if side.vol > 0.0 {
338 lines.push(format!(
339 "{label}: {:+.2} bps (net {}$ on ${})",
340 side.net / side.vol * 1e4,
341 commas_signed(side.net),
342 commas(side.vol)
343 ));
344 }
345 }
346 lines
347 }
348
349 fn venue_table(&self) -> Vec<String> {
350 let mut lines = vec![
351 "| Venue | our share | L1 share | vw bps (our legs) |".to_string(),
352 "|---|---|---|---|".to_string(),
353 ];
354 let total_us: f64 = self.venues.values().map(|e| e.us).sum::<f64>().max(1.0);
355 let total_l1: f64 = self.venues.values().map(|e| e.l1).sum::<f64>().max(1.0);
356 let mut ranked: Vec<_> = self.venues.iter().collect();
357 ranked.sort_by(|a, b| b.1.us.total_cmp(&a.1.us));
358 for (venue, e) in ranked {
359 if e.us < VENUE_DUST_USD && e.l1 < VENUE_DUST_USD {
360 continue;
361 }
362 let vw = if e.us == 0.0 { 0.0 } else { e.net / e.us * 1e4 };
363 lines.push(format!(
364 "| {venue} | {:.1}% | {:.1}% | {vw:+.2} |",
365 e.us / total_us * 100.0,
366 e.l1 / total_l1 * 100.0
367 ));
368 }
369 lines
370 }
371}
372
373fn commas(x: f64) -> String {
374 commas_u(x.round().abs() as u64)
375}
376
377fn commas_signed(x: f64) -> String {
378 format!("{}{}", if x < 0.0 { "-" } else { "+" }, commas(x))
379}
380
381#[cfg(test)]
382mod tests {
383 use rstest::rstest;
384
385 use super::*;
386 use crate::{
387 reroute_report::{
388 Samples,
389 testing::{hop, plan, record_of},
390 },
391 route_plan::RouteHop,
392 };
393
394 #[rstest]
395 #[case::rounds_up("+1,235", 1_234.6)]
396 #[case::negative("-1,235", -1_234.6)]
397 fn signed_commas_carry_the_sign(#[case] expected: &str, #[case] x: f64) {
398 assert_eq!(commas_signed(x), expected);
399 }
400
401 fn hop_of(input_mint: &Address, in_amount: u64) -> RouteHop {
402 hop(
403 None,
404 "Venue",
405 &input_mint.to_string(),
406 &mints::USDT.to_string(),
407 in_amount,
408 )
409 }
410
411 fn amount_hop(in_amount: Option<&str>) -> RouteHop {
413 let mut hop = hop_of(&mints::USDC, 0);
414 hop.swap_info.in_amount = in_amount.map(str::to_string);
415 hop
416 }
417
418 #[rstest]
419 #[case::integer_amount(Some("1000"), Some(1_000))]
420 #[case::float_amount_truncates(Some("1000.9"), Some(1_000))]
421 #[case::float_string_truncates(Some("1.5e3"), Some(1_500))]
422 #[case::unparseable_voids_the_plan(Some("abc"), None)]
423 #[case::an_absent_amount_alone_sums_to_nothing(None, None)]
424 fn plan_input_reads_integer_and_float_amounts(
425 #[case] amount: Option<&str>,
426 #[case] expected: Option<i64>,
427 ) {
428 let plan = plan(vec![amount_hop(amount)]).expect("a plan");
429 assert_eq!(plan_input(&plan, &mints::USDC), expected);
430 }
431
432 #[test]
433 fn plan_input_sums_only_the_asked_mints_hops() {
434 let plan = plan(vec![
435 hop_of(&mints::USDC, 300),
436 hop_of(&Address::default(), 999),
437 hop_of(&mints::USDC, 700),
438 ])
439 .expect("a plan");
440 assert_eq!(plan_input(&plan, &mints::USDC), Some(1_000));
441
442 let empty: RoutePlan = serde_json::from_str("[]").expect("an empty plan");
444 assert_eq!(plan_input(&empty, &mints::USDC), None);
445 }
446
447 #[test]
448 fn an_absent_amount_contributes_zero_without_voiding() {
449 let plan = plan(vec![amount_hop(None), hop_of(&mints::USDC, 700)]).expect("a plan");
450 assert_eq!(plan_input(&plan, &mints::USDC), Some(700));
451 }
452
453 fn prices() -> Prices {
456 let mut samples = Samples::default();
457 for n in 1..=3u64 {
458 samples.push(&LegRecord {
459 original_realized_output_amount: Some(n * 100_000_000),
460 ..record_of(mints::SOL, mints::USDC, n * 1_000_000_000, None, None, None)
461 });
462 }
463 samples.prices().expect("anchored")
464 }
465
466 fn stable_record(mode: SwapMode, ours: u64, l1: u64) -> LegRecord {
467 LegRecord {
468 swap_mode: mode,
469 realized_output_amount: Some(ours),
470 original_realized_output_amount: Some(l1),
471 route_plan: plan(vec![hop_of(&mints::USDC, ours)]),
472 original_route_plan: plan(vec![hop_of(&mints::USDC, l1)]),
473 ..record_of(mints::USDC, mints::USDT, 1_000_000, None, None, None)
474 }
475 }
476
477 #[test]
478 fn exact_out_credits_the_input_saved() {
479 let m = measure(
480 &stable_record(SwapMode::ExactOut, 999_000, 1_000_000),
481 &prices(),
482 )
483 .expect("measurable");
484 assert_eq!(m.outcome, Ordering::Greater);
485 assert!((m.l1_usd - 1.0).abs() < 1e-9);
486 assert!((m.our_usd - 1.001).abs() < 1e-9);
487 }
488
489 #[rstest]
490 #[case::win(1_010_000, 1_000_000, Some(Ordering::Greater))]
491 #[case::tie(1_000_000, 1_000_000, Some(Ordering::Equal))]
492 #[case::exactly_at_the_outlier_bound_stays(1_200_000, 1_000_000, Some(Ordering::Greater))]
493 #[case::past_the_outlier_bound_drops(1_210_000, 1_000_000, None)]
494 fn exact_in_measures_major_output_with_the_outlier_gate(
495 #[case] ours: u64,
496 #[case] l1: u64,
497 #[case] expected: Option<Ordering>,
498 ) {
499 let m = measure(&stable_record(SwapMode::ExactIn, ours, l1), &prices());
500 assert_eq!(m.map(|m| m.outcome), expected);
501 }
502
503 #[test]
505 fn collect_drops_multi_row_signatures_wholesale() {
506 let mut a = stable_record(SwapMode::ExactIn, 1_000_000, 999_000);
507 a.original_signature = "sigA".to_string();
508 let mut a2 = a.clone();
509 a2.amount = 2_000_000;
510 let mut b = stable_record(SwapMode::ExactIn, 1_000_000, 999_000);
511 b.original_signature = "sigB".to_string();
512
513 let (kept, stats) = collect(vec![a, a2, b]);
514 assert_eq!(kept.len(), 1);
515 assert_eq!(kept[0].original_signature, "sigB");
516 assert_eq!(stats.dropped_multi_row, 1);
517 }
518
519 #[test]
523 fn collect_skips_exact_out_legs_it_cannot_price() {
524 let exotic = LegRecord {
525 input_mint: Address::default(),
526 ..stable_record(SwapMode::ExactOut, 1_000_000, 999_000)
527 };
528 let major = stable_record(SwapMode::ExactOut, 1_000_000, 999_000);
529
530 let (kept, stats) = collect(vec![exotic, major]);
531 assert_eq!(kept.len(), 1);
532 assert_eq!(kept[0].input_mint, mints::USDC);
533 assert_eq!(stats.dropped_multi_row, 0, "a skipped row is not a pairing");
534 }
535}