280 lines
9.8 KiB
Rust
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);
|
|
}
|
|
}
|