0.7.24
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user