0.7.28
This commit is contained in:
279
kb_lib/src/trade_metric_update.rs
Normal file
279
kb_lib/src/trade_metric_update.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user