This commit is contained in:
2026-05-05 05:03:11 +02:00
parent 3e994995d7
commit f2c227e08f
132 changed files with 5767 additions and 4461 deletions

View File

@@ -17,6 +17,12 @@ pub struct KbTradeAggregationResult {
pub created_trade_event: bool,
}
type KbExtractedTradeAmounts = (
std::option::Option<std::string::String>,
std::option::Option<std::string::String>,
std::option::Option<f64>,
);
/// Trade-aggregation service.
#[derive(Debug, Clone)]
pub struct KbTradeAggregationService {
@@ -28,10 +34,7 @@ impl KbTradeAggregationService {
/// Creates a new trade-aggregation service.
pub fn new(database: std::sync::Arc<crate::KbDatabase>) -> Self {
let persistence = crate::KbDetectionPersistenceService::new(database.clone());
Self {
database,
persistence,
}
return Self { database, persistence };
}
/// Records normalized trade events and updates pair metrics for one transaction signature.
@@ -52,7 +55,7 @@ impl KbTradeAggregationService {
"cannot aggregate trades for unknown transaction '{}'",
signature
)));
}
},
};
let transaction_id = match transaction.id {
Some(transaction_id) => transaction_id,
@@ -61,7 +64,7 @@ impl KbTradeAggregationService {
"transaction '{}' has no internal id",
signature
)));
}
},
};
let decoded_events_result = crate::list_dex_decoded_events_by_transaction_id(
self.database.as_ref(),
@@ -83,7 +86,7 @@ impl KbTradeAggregationService {
return Err(crate::KbError::InvalidState(
"decoded event has no internal id".to_string(),
));
}
},
};
let existing_trade_result = crate::get_trade_event_by_decoded_event_id(
self.database.as_ref(),
@@ -115,7 +118,7 @@ impl KbTradeAggregationService {
"pool '{}' has no internal id",
pool.address
)));
}
},
};
let pair_result = crate::get_pair_by_pool_id(self.database.as_ref(), pool_id).await;
let pair_option = match pair_result {
@@ -133,7 +136,7 @@ impl KbTradeAggregationService {
"pair for pool '{}' has no internal id",
pool_id
)));
}
},
};
let pool_tokens_result =
crate::list_pool_tokens_by_pool_id(self.database.as_ref(), pool_id).await;
@@ -158,7 +161,7 @@ impl KbTradeAggregationService {
"skipping decoded event with invalid payload_json"
);
continue;
}
},
};
if !kb_is_decoded_event_trade_candidate(decoded_event.event_kind.as_str(), &payload) {
tracing::debug!(
@@ -181,13 +184,7 @@ impl KbTradeAggregationService {
let trade_side = kb_extract_trade_side(decoded_event.event_kind.as_str(), &payload);
let mut base_amount_raw = kb_extract_amount_string(
&payload,
&[
"baseAmountRaw",
"base_amount_raw",
"baseAmount",
"amountBase",
"amountInBase",
],
&["baseAmountRaw", "base_amount_raw", "baseAmount", "amountBase", "amountInBase"],
);
let mut quote_amount_raw = kb_extract_amount_string(
&payload,
@@ -377,7 +374,7 @@ impl KbTradeAggregationService {
return Err(crate::KbError::InvalidState(
"pair metric has no internal id".to_string(),
));
}
},
};
if created_trade_event {
let mut updated_metric = existing_metric.clone();
@@ -464,7 +461,7 @@ impl KbTradeAggregationService {
created_trade_event,
});
}
Ok(results)
return Ok(results);
}
}
@@ -481,7 +478,7 @@ fn kb_is_decoded_event_trade_candidate(event_kind: &str, payload: &serde_json::V
if let Some(event_category) = event_category_option {
return event_category.as_str() == "trade";
}
kb_is_trade_event_kind(event_kind)
return kb_is_trade_event_kind(event_kind);
}
fn kb_is_decoded_event_candle_candidate(event_kind: &str, payload: &serde_json::Value) -> bool {
@@ -495,7 +492,7 @@ fn kb_is_decoded_event_candle_candidate(event_kind: &str, payload: &serde_json::
if !kb_is_decoded_event_trade_candidate(event_kind, payload) {
return false;
}
kb_is_trade_event_kind(event_kind)
return kb_is_trade_event_kind(event_kind);
}
fn kb_extract_top_level_bool_by_candidate_keys(
@@ -537,7 +534,7 @@ fn kb_extract_top_level_bool_by_candidate_keys(
}
}
}
None
return None;
}
fn kb_is_priced_trade_event(
@@ -582,7 +579,7 @@ fn kb_is_priced_trade_event(
if !price.is_finite() {
return false;
}
price > 0.0
return price > 0.0;
}
fn kb_is_trade_event_kind(event_kind: &str) -> bool {
@@ -613,16 +610,16 @@ fn kb_is_trade_event_kind(event_kind: &str) -> bool {
if event_kind == "raydium_clmm.exact_output" {
return true;
}
false
return false;
}
fn kb_convert_slot_to_i64(slot: std::option::Option<u64>) -> std::option::Option<i64> {
match slot {
Some(slot) => match i64::try_from(slot) {
Ok(slot) => Some(slot),
Err(_) => None,
Ok(slot) => return Some(slot),
Err(_) => return None,
},
None => None,
None => return None,
}
}
@@ -636,7 +633,7 @@ fn kb_extract_trade_side(event_kind: &str, payload: &serde_json::Value) -> crate
Some("SellBase") => return crate::KbSwapTradeSide::SellBase,
Some("sell") => return crate::KbSwapTradeSide::SellBase,
Some("SELL") => return crate::KbSwapTradeSide::SellBase,
_ => {}
_ => {},
}
if event_kind.ends_with(".buy") {
return crate::KbSwapTradeSide::BuyBase;
@@ -644,14 +641,14 @@ fn kb_extract_trade_side(event_kind: &str, payload: &serde_json::Value) -> crate
if event_kind.ends_with(".sell") {
return crate::KbSwapTradeSide::SellBase;
}
crate::KbSwapTradeSide::Unknown
return crate::KbSwapTradeSide::Unknown;
}
fn kb_extract_amount_string(
payload: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<std::string::String> {
kb_extract_scalar_as_string_by_candidate_keys(payload, candidate_keys)
return kb_extract_scalar_as_string_by_candidate_keys(payload, candidate_keys);
}
fn kb_apply_trade_to_pair_metric(
@@ -693,9 +690,9 @@ fn kb_add_raw_amounts(
right: std::option::Option<std::string::String>,
) -> std::option::Option<std::string::String> {
match (left, right) {
(None, None) => None,
(Some(left), None) => Some(left),
(None, Some(right)) => Some(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 {
@@ -707,8 +704,8 @@ fn kb_add_raw_amounts(
Ok(right_value) => right_value,
Err(_) => return Some(left),
};
Some((left_value + right_value).to_string())
}
return Some((left_value + right_value).to_string());
},
}
}
@@ -742,7 +739,7 @@ fn kb_extract_string_by_candidate_keys(
}
}
}
None
return None;
}
fn kb_extract_scalar_as_string_by_candidate_keys(
@@ -785,7 +782,7 @@ fn kb_extract_scalar_as_string_by_candidate_keys(
}
}
}
None
return None;
}
fn kb_find_pool_token_vault_address_by_token_id(
@@ -806,7 +803,7 @@ fn kb_find_pool_token_vault_address_by_token_id(
}
return Some(vault_address);
}
None
return None;
}
fn kb_extract_trade_amounts_from_vault_balance_deltas(
@@ -814,14 +811,7 @@ fn kb_extract_trade_amounts_from_vault_balance_deltas(
meta_json: std::option::Option<&str>,
base_vault_address: std::option::Option<&str>,
quote_vault_address: std::option::Option<&str>,
) -> Result<
(
std::option::Option<std::string::String>,
std::option::Option<std::string::String>,
std::option::Option<f64>,
),
crate::KbError,
> {
) -> Result<KbExtractedTradeAmounts, crate::KbError> {
let meta_json = match meta_json {
Some(meta_json) => meta_json,
None => return Ok((None, None, None)),
@@ -834,7 +824,7 @@ fn kb_extract_trade_amounts_from_vault_balance_deltas(
"cannot parse transaction_json for pump_swap amount extraction: {}",
error
)));
}
},
};
let meta_value_result = serde_json::from_str::<serde_json::Value>(meta_json);
let meta_value = match meta_value_result {
@@ -844,7 +834,7 @@ fn kb_extract_trade_amounts_from_vault_balance_deltas(
"cannot parse meta_json for pump_swap amount extraction: {}",
error
)));
}
},
};
let account_keys_result = kb_extract_transaction_account_keys(&transaction_value);
let account_keys = match account_keys_result {
@@ -869,32 +859,29 @@ fn kb_extract_trade_amounts_from_vault_balance_deltas(
if let Some(base_vault_address) = base_vault_address {
let base_pre = pre_balances.get(base_vault_address);
let base_post = post_balances.get(base_vault_address);
let base_pre_raw = base_pre.map(|value| value.0.clone());
let base_post_raw = base_post.map(|value| value.0.clone());
let base_pre_raw = base_pre.map(|value| return value.0.clone());
let base_post_raw = base_post.map(|value| return value.0.clone());
base_amount_raw = kb_compute_amount_delta_abs(base_pre_raw, base_post_raw);
let base_pre_ui = base_pre.and_then(|value| value.1);
let base_post_ui = base_post.and_then(|value| value.1);
let base_pre_ui = base_pre.and_then(|value| return value.1);
let base_post_ui = base_post.and_then(|value| return value.1);
let base_delta_ui = kb_compute_ui_delta_abs(base_pre_ui, base_post_ui);
if let Some(quote_vault_address) = quote_vault_address {
let quote_pre = pre_balances.get(quote_vault_address);
let quote_post = post_balances.get(quote_vault_address);
let quote_pre_raw = quote_pre.map(|value| value.0.clone());
let quote_post_raw = quote_post.map(|value| value.0.clone());
let quote_pre_raw = quote_pre.map(|value| return value.0.clone());
let quote_post_raw = quote_post.map(|value| return value.0.clone());
quote_amount_raw = kb_compute_amount_delta_abs(quote_pre_raw, quote_post_raw);
let quote_pre_ui = quote_pre.and_then(|value| value.1);
let quote_post_ui = quote_post.and_then(|value| value.1);
let quote_pre_ui = quote_pre.and_then(|value| return value.1);
let quote_post_ui = quote_post.and_then(|value| return value.1);
let quote_delta_ui = kb_compute_ui_delta_abs(quote_pre_ui, quote_post_ui);
match (base_delta_ui, quote_delta_ui) {
(Some(base_delta_ui), Some(quote_delta_ui)) => {
if base_delta_ui > 0.0 {
price_quote_per_base = Some(quote_delta_ui / base_delta_ui);
}
if let (Some(base_delta_ui), Some(quote_delta_ui)) = (base_delta_ui, quote_delta_ui) {
if base_delta_ui > 0.0 {
price_quote_per_base = Some(quote_delta_ui / base_delta_ui);
}
_ => {}
}
}
}
Ok((base_amount_raw, quote_amount_raw, price_quote_per_base))
return Ok((base_amount_raw, quote_amount_raw, price_quote_per_base));
}
fn kb_extract_pump_fun_amounts_from_transaction(
@@ -902,14 +889,7 @@ fn kb_extract_pump_fun_amounts_from_transaction(
meta_json: std::option::Option<&str>,
base_vault_address: std::option::Option<&str>,
quote_native_address: std::option::Option<&str>,
) -> Result<
(
std::option::Option<std::string::String>,
std::option::Option<std::string::String>,
std::option::Option<f64>,
),
crate::KbError,
> {
) -> Result<KbExtractedTradeAmounts, crate::KbError> {
let meta_json = match meta_json {
Some(meta_json) => meta_json,
None => return Ok((None, None, None)),
@@ -922,7 +902,7 @@ fn kb_extract_pump_fun_amounts_from_transaction(
"cannot parse transaction_json for pump_fun amount extraction: {}",
error
)));
}
},
};
let meta_value_result = serde_json::from_str::<serde_json::Value>(meta_json);
let meta_value = match meta_value_result {
@@ -932,7 +912,7 @@ fn kb_extract_pump_fun_amounts_from_transaction(
"cannot parse meta_json for pump_fun amount extraction: {}",
error
)));
}
},
};
let account_keys_result = kb_extract_transaction_account_keys(&transaction_value);
let account_keys = match account_keys_result {
@@ -958,11 +938,11 @@ fn kb_extract_pump_fun_amounts_from_transaction(
if let Some(base_vault_address) = base_vault_address {
let base_pre = pre_balances.get(base_vault_address);
let base_post = post_balances.get(base_vault_address);
let base_pre_raw = base_pre.map(|value| value.0.clone());
let base_post_raw = base_post.map(|value| value.0.clone());
let base_pre_raw = base_pre.map(|value| return value.0.clone());
let base_post_raw = base_post.map(|value| return value.0.clone());
base_amount_raw = kb_compute_amount_delta_abs(base_pre_raw, base_post_raw);
let base_pre_ui = base_pre.and_then(|value| value.1);
let base_post_ui = base_post.and_then(|value| value.1);
let base_pre_ui = base_pre.and_then(|value| return value.1);
let base_post_ui = base_post.and_then(|value| return value.1);
base_delta_ui = kb_compute_ui_delta_abs(base_pre_ui, base_post_ui);
}
if let Some(quote_native_address) = quote_native_address {
@@ -985,7 +965,7 @@ fn kb_extract_pump_fun_amounts_from_transaction(
}
}
}
Ok((base_amount_raw, quote_amount_raw, price_quote_per_base))
return Ok((base_amount_raw, quote_amount_raw, price_quote_per_base));
}
fn kb_extract_native_balance_delta_by_address(
@@ -1004,12 +984,10 @@ fn kb_extract_native_balance_delta_by_address(
Some(account_index) => account_index,
None => return Ok(None),
};
let pre_balances_option = meta_value
.get("preBalances")
.and_then(|value| value.as_array());
let post_balances_option = meta_value
.get("postBalances")
.and_then(|value| value.as_array());
let pre_balances_option =
meta_value.get("preBalances").and_then(|value| return value.as_array());
let post_balances_option =
meta_value.get("postBalances").and_then(|value| return value.as_array());
let pre_balances = match pre_balances_option {
Some(pre_balances) => pre_balances,
None => return Ok(None),
@@ -1034,7 +1012,7 @@ fn kb_extract_native_balance_delta_by_address(
if post_balance >= pre_balance {
return Ok(Some(post_balance - pre_balance));
}
Ok(Some(pre_balance - post_balance))
return Ok(Some(pre_balance - post_balance));
}
fn kb_extract_transaction_account_keys(
@@ -1043,11 +1021,11 @@ fn kb_extract_transaction_account_keys(
let candidate_arrays = [
transaction_value
.get("message")
.and_then(|value| value.get("accountKeys")),
.and_then(|value| return value.get("accountKeys")),
transaction_value
.get("transaction")
.and_then(|value| value.get("message"))
.and_then(|value| value.get("accountKeys")),
.and_then(|value| return value.get("message"))
.and_then(|value| return value.get("accountKeys")),
transaction_value.get("accountKeys"),
];
for candidate_array_option in candidate_arrays {
@@ -1065,7 +1043,7 @@ fn kb_extract_transaction_account_keys(
account_keys.push(value.to_string());
continue;
}
let pubkey_option = item.get("pubkey").and_then(|value| value.as_str());
let pubkey_option = item.get("pubkey").and_then(|value| return value.as_str());
if let Some(pubkey) = pubkey_option {
account_keys.push(pubkey.to_string());
continue;
@@ -1075,9 +1053,9 @@ fn kb_extract_transaction_account_keys(
return Ok(account_keys);
}
}
Err(crate::KbError::Json(
return Err(crate::KbError::Json(
"cannot extract accountKeys from transaction_json".to_string(),
))
));
}
fn kb_extract_token_balance_map(
@@ -1095,15 +1073,14 @@ fn kb_extract_token_balance_map(
std::string::String,
(std::string::String, std::option::Option<f64>),
>::new();
let balances_option = meta_value
.get(field_name)
.and_then(|value| value.as_array());
let balances_option = meta_value.get(field_name).and_then(|value| return value.as_array());
let balances = match balances_option {
Some(balances) => balances,
None => return Ok(result),
};
for balance in balances {
let account_index_option = balance.get("accountIndex").and_then(|value| value.as_u64());
let account_index_option =
balance.get("accountIndex").and_then(|value| return value.as_u64());
let account_index = match account_index_option {
Some(account_index) => account_index as usize,
None => continue,
@@ -1117,16 +1094,14 @@ fn kb_extract_token_balance_map(
Some(ui_token_amount) => ui_token_amount,
None => continue,
};
let raw_amount_option = ui_token_amount
.get("amount")
.and_then(|value| value.as_str());
let raw_amount_option =
ui_token_amount.get("amount").and_then(|value| return value.as_str());
let raw_amount = match raw_amount_option {
Some(raw_amount) => raw_amount.to_string(),
None => continue,
};
let ui_amount_string_option = ui_token_amount
.get("uiAmountString")
.and_then(|value| value.as_str());
let ui_amount_string_option =
ui_token_amount.get("uiAmountString").and_then(|value| return value.as_str());
let ui_amount = match ui_amount_string_option {
Some(ui_amount_string) => {
let parse_result = ui_amount_string.parse::<f64>();
@@ -1134,12 +1109,12 @@ fn kb_extract_token_balance_map(
Ok(ui_amount) => Some(ui_amount),
Err(_) => None,
}
}
},
None => None,
};
result.insert(account_address, (raw_amount, ui_amount));
}
Ok(result)
return Ok(result);
}
fn kb_compute_amount_delta_abs(
@@ -1169,7 +1144,7 @@ fn kb_compute_amount_delta_abs(
} else {
pre_value - post_value
};
Some(delta.to_string())
return Some(delta.to_string());
}
fn kb_compute_ui_delta_abs(
@@ -1189,7 +1164,7 @@ fn kb_compute_ui_delta_abs(
} else {
pre_amount - post_amount
};
Some(delta)
return Some(delta);
}
fn kb_compute_price_quote_per_base_from_raw_amounts(
@@ -1220,7 +1195,7 @@ fn kb_compute_price_quote_per_base_from_raw_amounts(
if base_amount <= 0.0 {
return None;
}
Some(quote_amount / base_amount)
return Some(quote_amount / base_amount);
}
fn kb_compute_price_quote_per_base_with_decimals(
@@ -1239,7 +1214,7 @@ fn kb_compute_price_quote_per_base_with_decimals(
Ok(inferred) => inferred,
Err(_) => return None,
};
inferred.2
return inferred.2;
}
#[cfg(test)]
@@ -1268,7 +1243,7 @@ mod tests {
Ok(database) => database,
Err(error) => panic!("database init must succeed: {}", error),
};
std::sync::Arc::new(database)
return std::sync::Arc::new(database);
}
async fn seed_fluxbeam_swap_transaction(
@@ -1346,9 +1321,8 @@ mod tests {
seed_fluxbeam_swap_transaction(database.clone(), "sig-trade-aggregation-1", "1000", "2500")
.await;
let service = crate::KbTradeAggregationService::new(database.clone());
let record_result = service
.record_transaction_by_signature("sig-trade-aggregation-1")
.await;
let record_result =
service.record_transaction_by_signature("sig-trade-aggregation-1").await;
let results = match record_result {
Ok(results) => results,
Err(error) => panic!("trade aggregation must succeed: {}", error),
@@ -1379,14 +1353,8 @@ mod tests {
assert_eq!(pair_metric.trade_count, 1);
assert_eq!(pair_metric.buy_count, 1);
assert_eq!(pair_metric.sell_count, 0);
assert_eq!(
pair_metric.cumulative_base_amount_raw,
Some("1000".to_string())
);
assert_eq!(
pair_metric.cumulative_quote_amount_raw,
Some("2500".to_string())
);
assert_eq!(pair_metric.cumulative_base_amount_raw, Some("1000".to_string()));
assert_eq!(pair_metric.cumulative_quote_amount_raw, Some("2500".to_string()));
assert_eq!(pair_metric.last_price_quote_per_base, Some(2.5));
}
@@ -1396,18 +1364,15 @@ mod tests {
seed_fluxbeam_swap_transaction(database.clone(), "sig-trade-aggregation-2", "1000", "2500")
.await;
let service = crate::KbTradeAggregationService::new(database.clone());
let first_result = service
.record_transaction_by_signature("sig-trade-aggregation-2")
.await;
let first_result = service.record_transaction_by_signature("sig-trade-aggregation-2").await;
let first_results = match first_result {
Ok(first_results) => first_results,
Err(error) => panic!("first trade aggregation must succeed: {}", error),
};
assert_eq!(first_results.len(), 1);
assert!(first_results[0].created_trade_event);
let second_result = service
.record_transaction_by_signature("sig-trade-aggregation-2")
.await;
let second_result =
service.record_transaction_by_signature("sig-trade-aggregation-2").await;
let second_results = match second_result {
Ok(second_results) => second_results,
Err(error) => panic!("second trade aggregation must succeed: {}", error),