Files
khadhroony-bobobot/kb_lib/src/trade_metric_update.rs
2026-05-11 11:02:47 +02:00

280 lines
9.8 KiB
Rust

// 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<f64>,
) -> 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::<i128>();
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::<i128>();
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<u64>) -> std::option::Option<i64> {
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<i64>,
signature: std::string::String,
trade_side: crate::SwapTradeSide,
base_amount_raw: std::option::Option<std::string::String>,
quote_amount_raw: std::option::Option<std::string::String>,
price_quote_per_base: std::option::Option<f64>,
) {
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<std::string::String>,
right: std::option::Option<std::string::String>,
) -> std::option::Option<std::string::String> {
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::<i128>();
let left_value = match left_value_result {
Ok(left_value) => left_value,
Err(_) => return Some(left),
};
let right_value_result = right.parse::<i128>();
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<u8>,
quote_decimals: std::option::Option<u8>,
) -> std::option::Option<f64> {
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::<f64>();
let base_amount = match base_amount_result {
Ok(base_amount) => base_amount,
Err(_) => return None,
};
let quote_amount_result = quote_amount_raw.parse::<f64>();
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<f64> {
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::<f64>();
let base_amount = match base_amount_result {
Ok(base_amount) => base_amount,
Err(_) => return None,
};
let quote_amount_result = quote_amount_raw.parse::<f64>();
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);
}
}