//! End-to-end: a real captured payload becomes a decision and a ledger entry. //! //! These tests deliberately use the *checked-in captures* rather than //! synthetic data. A unit test proves the arithmetic; only real payloads //! prove the field names, the camelCase, and the inverted `tradeType` are //! all handled — and those are the things that actually broke. use p2p_analyzer::analyse; use p2p_core::{Price, QualityFilter, Side}; use p2p_scanner::{parse_side, snapshot}; use p2p_tracker::{Ledger, Trade}; fn fixture(name: &str) -> String { let path = concat!(env!("CARGO_MANIFEST_DIR"), "/fixtures/"); std::fs::read_to_string(format!("{path}{name}")).expect("fixture should exist") } fn p(s: &str) -> Price { Price::parse(s).unwrap() } /// The whole pipeline over the real KES capture. #[test] fn a_real_capture_flows_from_json_to_a_spread_decision() { let buys = parse_side(&fixture("kes_buy_side.json"), Side::WeBuy, "t").unwrap(); let sells = parse_side(&fixture("kes_sell_side.json"), Side::WeSell, "t").unwrap(); let snap = snapshot("KES", "USDT", buys, sells, 0); assert!(!snap.is_empty_market()); let report = analyse(&snap, &QualityFilter::default(), 0); // The honest result, and the whole point of the exercise: at the default // quality floor (95% completion, 50+ orders) **not one sell-side advert // in the live KES book qualified**. Their order counts were 3, 48, 25, 2 // and 0. The buy side was full of merchants with thousands of trades. // // So there was no fillable arbitrage in this capture. A tool that // reported the raw best-price spread would have said 3.6%. assert!( report.best_buy.is_some(), "the buy side had well-established merchants" ); assert!( report.best_sell.is_none(), "no sell-side advert cleared the quality floor" ); assert_eq!( report.net_bps, None, "no qualifying counterparty means no spread, not a spread of zero" ); assert!(!report.is_actionable(0)); assert_eq!( report.rejected.len(), 5, "every sell-side advert should be reported as excluded, with a reason" ); } /// The finding that motivated the quality filter, asserted end to end. #[test] fn the_unfiltered_spread_is_much_wider_than_the_fillable_one() { let buys = parse_side(&fixture("kes_buy_side.json"), Side::WeBuy, "t").unwrap(); let sells = parse_side(&fixture("kes_sell_side.json"), Side::WeSell, "t").unwrap(); let snap = snapshot("KES", "USDT", buys, sells, 0); let lenient = QualityFilter { min_finish_rate_bps: 0, min_order_count: 0, min_tradable_fiat: None, }; let naive = analyse(&snap, &lenient, 0); let naive_bps = naive.gross_bps.expect("a naive scan always finds a number"); // Unfiltered, this book reads as a 3.6% opportunity — the kind of number // that gets someone to move real money. assert!(naive_bps > 300, "naive reads as a >3% opportunity"); assert_eq!(naive.best_sell.unwrap().advertiser, "tiero"); // Filtered, it disappears entirely, because the advert offering 134.60 // had completed three trades. let strict = analyse(&snap, &QualityFilter::default(), 0); assert_eq!( strict.gross_bps, None, "the apparent opportunity was entirely made of unqualified counterparties" ); // A moderate floor keeps the book's one semi-established seller (48 // orders, 87% completion) and reports a realistic 29 bps instead. let moderate = QualityFilter { min_finish_rate_bps: 8000, min_order_count: 40, min_tradable_fiat: None, }; assert_eq!(analyse(&snap, &moderate, 0).gross_bps, Some(29)); } /// An empty market must be reported as empty, not as an error or a spread. #[test] fn an_empty_market_produces_no_spread_and_no_panic() { let ads = parse_side(&fixture("ngn_empty_market.json"), Side::WeBuy, "t").unwrap(); assert!(ads.is_empty()); let snap = snapshot("NGN", "USDT", ads, vec![], 0); assert!(snap.is_empty_market()); let report = analyse(&snap, &QualityFilter::default(), 0); assert_eq!(report.net_bps, None); assert!(!report.is_actionable(0)); assert!(report.empty_market); } /// Buying and selling at the observed prices books the observed margin. #[test] fn a_round_trip_at_captured_prices_books_the_expected_profit() { let mut ledger = Ledger::default(); ledger.record(Trade { id: "buy-1".into(), fiat: "KES".into(), side: Side::WeBuy, units: p("1000"), price: p("129.92"), counterparty: "BennyBoss".into(), method: "MpesaPaybill".into(), executed_at_ms: 1, }); ledger.record(Trade { id: "sell-1".into(), fiat: "KES".into(), side: Side::WeSell, units: p("1000"), price: p("130.30"), counterparty: "Twin_traders00".into(), method: "BANK".into(), executed_at_ms: 2, }); let inv = ledger.replay("KES").unwrap(); // 1000 * (130.30 - 129.92) = 380 KES assert_eq!(inv.realised_pnl, p("380")); assert_eq!(inv.units, Price::ZERO); assert_eq!(inv.average_cost(), None); } /// The ledger survives a save/load cycle with exact values. #[test] fn the_ledger_round_trips_through_disk_without_drift() { let mut ledger = Ledger::default(); for i in 0..50 { ledger.record(Trade { id: format!("t{i}"), fiat: "KES".into(), side: if i % 2 == 0 { Side::WeBuy } else { Side::WeSell }, units: p("10"), price: if i % 2 == 0 { p("129.92") } else { p("130.30") }, counterparty: "someone".into(), method: "BANK".into(), executed_at_ms: i as i64, }); } let before = ledger.replay("KES").unwrap(); let reloaded = Ledger::from_json(&ledger.to_json(), "mem").unwrap(); let after = reloaded.replay("KES").unwrap(); assert_eq!(before, after, "serialisation must not perturb the P&L"); // 25 round trips of 10 USDT at a 0.38 margin = 95 KES exactly. assert_eq!(after.realised_pnl, p("95")); } /// The direction guard, end to end: our buy side must be cheaper. #[test] fn the_two_sides_of_the_capture_are_not_the_same_data() { let buys = parse_side(&fixture("kes_buy_side.json"), Side::WeBuy, "t").unwrap(); let sells = parse_side(&fixture("kes_sell_side.json"), Side::WeSell, "t").unwrap(); let cheapest_buy = buys.iter().map(|a| a.price).min().unwrap(); let dearest_sell = sells.iter().map(|a| a.price).max().unwrap(); assert!( dearest_sell > cheapest_buy, "a book where you cannot buy below the best bid is a parsing bug" ); assert!(buys.iter().all(|a| a.side == Side::WeBuy)); assert!(sells.iter().all(|a| a.side == Side::WeSell)); }