// file: kb_lib/src/trade_metric_update.rs //! Trade metric update and basic trade-pricing helpers. //! //! This module contains pure helpers used by trade aggregation: //! pricing validation, raw amount accumulation and pair metric updates. /// Returns true when a decoded trade has enough positive values to be persisted. pub(crate) fn is_priced_trade_event( base_amount_raw: std::option::Option<&str>, quote_amount_raw: std::option::Option<&str>, price_quote_per_base: std::option::Option, ) -> bool { let base_amount_raw = match base_amount_raw { Some(base_amount_raw) => base_amount_raw.trim(), None => return false, }; if base_amount_raw.is_empty() { return false; } let base_amount_result = base_amount_raw.parse::(); let base_amount = match base_amount_result { Ok(base_amount) => base_amount, Err(_) => return false, }; if base_amount <= 0 { return false; } let quote_amount_raw = match quote_amount_raw { Some(quote_amount_raw) => quote_amount_raw.trim(), None => return false, }; if quote_amount_raw.is_empty() { return false; } let quote_amount_result = quote_amount_raw.parse::(); let quote_amount = match quote_amount_result { Ok(quote_amount) => quote_amount, Err(_) => return false, }; if quote_amount <= 0 { return false; } let price = match price_quote_per_base { Some(price) => price, None => return false, }; if !price.is_finite() { return false; } return price > 0.0; } /// Converts an optional Solana slot to an optional signed database slot. pub(crate) fn convert_slot_to_i64(slot: std::option::Option) -> std::option::Option { match slot { Some(slot) => match i64::try_from(slot) { Ok(slot) => return Some(slot), Err(_) => return None, }, None => return None, } } /// Applies one newly-created trade event to a pair metric. pub(crate) fn apply_trade_to_pair_metric( metric: &mut crate::PairMetricDto, slot: std::option::Option, signature: std::string::String, trade_side: crate::SwapTradeSide, base_amount_raw: std::option::Option, quote_amount_raw: std::option::Option, price_quote_per_base: std::option::Option, ) { metric.trade_count += 1; if trade_side == crate::SwapTradeSide::BuyBase { metric.buy_count += 1; } if trade_side == crate::SwapTradeSide::SellBase { metric.sell_count += 1; } if metric.first_slot.is_none() { metric.first_slot = slot; } if metric.first_signature.is_none() { metric.first_signature = Some(signature.clone()); } metric.last_slot = slot; metric.last_signature = Some(signature); metric.cumulative_base_amount_raw = crate::trade_metric_update::add_raw_amounts( metric.cumulative_base_amount_raw.clone(), base_amount_raw, ); metric.cumulative_quote_amount_raw = crate::trade_metric_update::add_raw_amounts( metric.cumulative_quote_amount_raw.clone(), quote_amount_raw, ); if price_quote_per_base.is_some() { metric.last_price_quote_per_base = price_quote_per_base; } metric.updated_at = chrono::Utc::now(); } /// Adds two optional raw integer amount strings. pub(crate) fn add_raw_amounts( left: std::option::Option, right: std::option::Option, ) -> std::option::Option { match (left, right) { (None, None) => return None, (Some(left), None) => return Some(left), (None, Some(right)) => return Some(right), (Some(left), Some(right)) => { let left_value_result = left.parse::(); let left_value = match left_value_result { Ok(left_value) => left_value, Err(_) => return Some(left), }; let right_value_result = right.parse::(); let right_value = match right_value_result { Ok(right_value) => right_value, Err(_) => return Some(left), }; return Some((left_value + right_value).to_string()); }, } } /// Computes quote/base price from raw amounts and token decimals. pub(crate) fn compute_price_quote_per_base_from_raw_amounts_with_decimals( base_amount_raw: std::option::Option<&str>, quote_amount_raw: std::option::Option<&str>, base_decimals: std::option::Option, quote_decimals: std::option::Option, ) -> std::option::Option { let base_decimals = match base_decimals { Some(base_decimals) => base_decimals, None => return None, }; let quote_decimals = match quote_decimals { Some(quote_decimals) => quote_decimals, None => return None, }; let base_amount_raw = match base_amount_raw { Some(base_amount_raw) => base_amount_raw.trim(), None => return None, }; let quote_amount_raw = match quote_amount_raw { Some(quote_amount_raw) => quote_amount_raw.trim(), None => return None, }; if base_amount_raw.is_empty() || quote_amount_raw.is_empty() { return None; } let base_amount_result = base_amount_raw.parse::(); let base_amount = match base_amount_result { Ok(base_amount) => base_amount, Err(_) => return None, }; let quote_amount_result = quote_amount_raw.parse::(); let quote_amount = match quote_amount_result { Ok(quote_amount) => quote_amount, Err(_) => return None, }; if base_amount <= 0.0 || quote_amount <= 0.0 { return None; } let base_scale = 10_f64.powi(i32::from(base_decimals)); let quote_scale = 10_f64.powi(i32::from(quote_decimals)); if base_scale <= 0.0 || quote_scale <= 0.0 { return None; } let base_ui_amount = base_amount / base_scale; let quote_ui_amount = quote_amount / quote_scale; if base_ui_amount <= 0.0 || quote_ui_amount <= 0.0 { return None; } return Some(quote_ui_amount / base_ui_amount); } /// Computes quote/base price from raw amount strings without decimals. pub(crate) fn compute_price_quote_per_base_from_raw_amounts( base_amount_raw: std::option::Option<&str>, quote_amount_raw: std::option::Option<&str>, ) -> std::option::Option { let base_amount_raw = match base_amount_raw { Some(base_amount_raw) => base_amount_raw.trim(), None => return None, }; let quote_amount_raw = match quote_amount_raw { Some(quote_amount_raw) => quote_amount_raw.trim(), None => return None, }; if base_amount_raw.is_empty() || quote_amount_raw.is_empty() { return None; } let base_amount_result = base_amount_raw.parse::(); let base_amount = match base_amount_result { Ok(base_amount) => base_amount, Err(_) => return None, }; let quote_amount_result = quote_amount_raw.parse::(); let quote_amount = match quote_amount_result { Ok(quote_amount) => quote_amount, Err(_) => return None, }; if base_amount <= 0.0 { return None; } return Some(quote_amount / base_amount); } #[cfg(test)] mod tests { #[test] fn priced_trade_event_rejects_unpriced_values() { let result = super::is_priced_trade_event(None, Some("2500"), Some(2.5)); assert!(!result); let result = super::is_priced_trade_event(Some("1000"), None, Some(2.5)); assert!(!result); let result = super::is_priced_trade_event(Some("1000"), Some("2500"), None); assert!(!result); let result = super::is_priced_trade_event(Some("0"), Some("2500"), Some(2.5)); assert!(!result); let result = super::is_priced_trade_event(Some("1000"), Some("0"), Some(2.5)); assert!(!result); let result = super::is_priced_trade_event(Some("-1"), Some("2500"), Some(2.5)); assert!(!result); let result = super::is_priced_trade_event(Some("1000"), Some("-1"), Some(2.5)); assert!(!result); let result = super::is_priced_trade_event(Some("abc"), Some("2500"), Some(2.5)); assert!(!result); let result = super::is_priced_trade_event(Some("1000"), Some("abc"), Some(2.5)); assert!(!result); let result = super::is_priced_trade_event(Some("1000"), Some("2500"), Some(0.0)); assert!(!result); let result = super::is_priced_trade_event(Some("1000"), Some("2500"), Some(f64::NAN)); assert!(!result); let result = super::is_priced_trade_event(Some("1000"), Some("2500"), Some(2.5)); assert!(result); } #[test] fn raw_amounts_are_added_when_both_are_valid() { let result = super::add_raw_amounts(Some("1000".to_string()), Some("2500".to_string())); assert_eq!(result, Some("3500".to_string())); } #[test] fn raw_amount_addition_keeps_left_when_right_is_invalid() { let result = super::add_raw_amounts(Some("1000".to_string()), Some("abc".to_string())); assert_eq!(result, Some("1000".to_string())); } #[test] fn price_with_decimals_is_computed() { let price = super::compute_price_quote_per_base_from_raw_amounts_with_decimals( Some("1000000"), Some("2500000000"), Some(6), Some(9), ); assert_eq!(price, Some(2.5)); } #[test] fn price_without_decimals_is_computed() { let price = super::compute_price_quote_per_base_from_raw_amounts(Some("1000"), Some("2500")); assert_eq!(price, Some(2.5)); } #[test] fn overflowing_slot_is_ignored() { let slot = super::convert_slot_to_i64(Some(u64::MAX)); assert_eq!(slot, None); } }