This commit is contained in:
2026-05-03 18:05:32 +02:00
parent 29ebf6b123
commit 3e994995d7
8 changed files with 1765 additions and 145 deletions

View File

@@ -150,12 +150,34 @@ impl KbTradeAggregationService {
let payload = match payload_result {
Ok(payload) => payload,
Err(error) => {
return Err(crate::KbError::Json(format!(
"cannot parse decoded_event payload_json '{}': {}",
decoded_event.payload_json, error
)));
tracing::warn!(
event_kind = %decoded_event.event_kind,
pool_account = ?decoded_event.pool_account,
decoded_event_id = ?decoded_event.id,
error = %error,
"skipping decoded event with invalid payload_json"
);
continue;
}
};
if !kb_is_decoded_event_trade_candidate(decoded_event.event_kind.as_str(), &payload) {
tracing::debug!(
event_kind = %decoded_event.event_kind,
pool_account = ?decoded_event.pool_account,
decoded_event_id = ?decoded_event.id,
"skipping non-trade decoded event"
);
continue;
}
if !kb_is_decoded_event_candle_candidate(decoded_event.event_kind.as_str(), &payload) {
tracing::debug!(
event_kind = %decoded_event.event_kind,
pool_account = ?decoded_event.pool_account,
decoded_event_id = ?decoded_event.id,
"skipping non-candle decoded trade candidate"
);
continue;
}
let trade_side = kb_extract_trade_side(decoded_event.event_kind.as_str(), &payload);
let mut base_amount_raw = kb_extract_amount_string(
&payload,
@@ -253,6 +275,31 @@ impl KbTradeAggregationService {
price_quote_per_base = inferred.2;
}
}
if decoded_event.event_kind.starts_with("raydium_clmm.")
&& (base_amount_raw.is_none()
|| quote_amount_raw.is_none()
|| price_quote_per_base.is_none())
{
let inferred_result = kb_extract_trade_amounts_from_vault_balance_deltas(
transaction.transaction_json.as_str(),
transaction.meta_json.as_deref(),
base_vault_address.as_deref(),
quote_vault_address.as_deref(),
);
let inferred = match inferred_result {
Ok(inferred) => inferred,
Err(error) => return Err(error),
};
if base_amount_raw.is_none() {
base_amount_raw = inferred.0;
}
if quote_amount_raw.is_none() {
quote_amount_raw = inferred.1;
}
if price_quote_per_base.is_none() {
price_quote_per_base = inferred.2;
}
}
if price_quote_per_base.is_none() {
price_quote_per_base = kb_compute_price_quote_per_base_with_decimals(
transaction.meta_json.as_deref(),
@@ -267,6 +314,23 @@ impl KbTradeAggregationService {
quote_amount_raw.as_deref(),
);
}
if !kb_is_priced_trade_event(
base_amount_raw.as_deref(),
quote_amount_raw.as_deref(),
price_quote_per_base,
) {
tracing::debug!(
event_kind = %decoded_event.event_kind,
pool_account = ?decoded_event.pool_account,
decoded_event_id = ?decoded_event.id,
transaction_signature = %transaction.signature,
base_amount_raw = ?base_amount_raw,
quote_amount_raw = ?quote_amount_raw,
price_quote_per_base = ?price_quote_per_base,
"skipping unpriced trade aggregation candidate"
);
continue;
}
let slot_i64 = kb_convert_slot_to_i64(transaction.slot);
let created_trade_event = existing_trade_option.is_none();
let trade_event_dto = crate::KbTradeEventDto::new(
@@ -404,6 +468,123 @@ impl KbTradeAggregationService {
}
}
fn kb_is_decoded_event_trade_candidate(event_kind: &str, payload: &serde_json::Value) -> bool {
let trade_candidate_option = kb_extract_top_level_bool_by_candidate_keys(
payload,
&["tradeCandidate", "trade_candidate"],
);
if let Some(trade_candidate) = trade_candidate_option {
return trade_candidate;
}
let event_category_option =
kb_extract_string_by_candidate_keys(payload, &["eventCategory", "event_category"]);
if let Some(event_category) = event_category_option {
return event_category.as_str() == "trade";
}
kb_is_trade_event_kind(event_kind)
}
fn kb_is_decoded_event_candle_candidate(event_kind: &str, payload: &serde_json::Value) -> bool {
let candle_candidate_option = kb_extract_top_level_bool_by_candidate_keys(
payload,
&["candleCandidate", "candle_candidate"],
);
if let Some(candle_candidate) = candle_candidate_option {
return candle_candidate;
}
if !kb_is_decoded_event_trade_candidate(event_kind, payload) {
return false;
}
kb_is_trade_event_kind(event_kind)
}
fn kb_extract_top_level_bool_by_candidate_keys(
payload: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<bool> {
let object = match payload.as_object() {
Some(object) => object,
None => return None,
};
for candidate_key in candidate_keys {
let value_option = object.get(*candidate_key);
let value = match value_option {
Some(value) => value,
None => continue,
};
if let Some(value_bool) = value.as_bool() {
return Some(value_bool);
}
if let Some(value_i64) = value.as_i64() {
return Some(value_i64 != 0);
}
if let Some(value_u64) = value.as_u64() {
return Some(value_u64 != 0);
}
if let Some(value_text) = value.as_str() {
let normalized = value_text.trim().to_ascii_lowercase();
if normalized.as_str() == "true" {
return Some(true);
}
if normalized.as_str() == "false" {
return Some(false);
}
if normalized.as_str() == "1" {
return Some(true);
}
if normalized.as_str() == "0" {
return Some(false);
}
}
}
None
}
fn kb_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;
}
price > 0.0
}
fn kb_is_trade_event_kind(event_kind: &str) -> bool {
if event_kind.ends_with(".swap") {
return true;
@@ -420,6 +601,18 @@ fn kb_is_trade_event_kind(event_kind: &str) -> bool {
if event_kind == "raydium_cpmm.swap_base_output" {
return true;
}
if event_kind == "raydium_clmm.swap_v2" {
return true;
}
if event_kind == "raydium_clmm.swap_router_base_in" {
return true;
}
if event_kind == "raydium_clmm.swap_router_base_out" {
return true;
}
if event_kind == "raydium_clmm.exact_output" {
return true;
}
false
}
@@ -1240,4 +1433,30 @@ mod tests {
};
assert_eq!(pair_metric.trade_count, 1);
}
#[test]
fn kb_is_priced_trade_event_rejects_unpriced_values() {
let result = super::kb_is_priced_trade_event(None, Some("2500"), Some(2.5));
assert!(!result);
let result = super::kb_is_priced_trade_event(Some("1000"), None, Some(2.5));
assert!(!result);
let result = super::kb_is_priced_trade_event(Some("1000"), Some("2500"), None);
assert!(!result);
let result = super::kb_is_priced_trade_event(Some("0"), Some("2500"), Some(2.5));
assert!(!result);
let result = super::kb_is_priced_trade_event(Some("1000"), Some("0"), Some(2.5));
assert!(!result);
let result = super::kb_is_priced_trade_event(Some("-1"), Some("2500"), Some(2.5));
assert!(!result);
let result = super::kb_is_priced_trade_event(Some("1000"), Some("-1"), Some(2.5));
assert!(!result);
let result = super::kb_is_priced_trade_event(Some("abc"), Some("2500"), Some(2.5));
assert!(!result);
let result = super::kb_is_priced_trade_event(Some("1000"), Some("abc"), Some(2.5));
assert!(!result);
let result = super::kb_is_priced_trade_event(Some("1000"), Some("2500"), Some(0.0));
assert!(!result);
let result = super::kb_is_priced_trade_event(Some("1000"), Some("2500"), Some(f64::NAN));
assert!(!result);
}
}