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