This commit is contained in:
2026-05-12 21:40:03 +02:00
parent 75c2b6983d
commit aa19ca9c18
21 changed files with 899 additions and 20 deletions

View File

@@ -32,6 +32,7 @@ pub use dtos::LiquidityEventDto;
pub use dtos::LocalDecodedEventDiagnosticSummaryDto;
pub use dtos::LocalDexDiagnosticSummaryDto;
pub use dtos::LocalDuplicateDecodedEventTradeDiagnosticSampleDto;
pub use dtos::LocalEventClassificationDiagnosticSummaryDto;
pub use dtos::LocalMissingTradeEventDiagnosticSampleDto;
pub use dtos::LocalMissingTradeEventReasonSummaryDto;
pub use dtos::LocalMultiTradeSignaturePairDiagnosticSampleDto;
@@ -144,6 +145,7 @@ pub use queries::query_liquidity_events_list_recent;
pub use queries::query_liquidity_events_upsert;
pub use queries::query_local_decoded_event_diagnostic_list_summaries;
pub use queries::query_local_duplicate_decoded_event_trade_diagnostic_list_samples;
pub use queries::query_local_event_classification_diagnostic_list_summaries;
pub use queries::query_local_missing_trade_event_diagnostic_list_samples;
pub use queries::query_local_missing_trade_event_reason_list_summaries;
pub use queries::query_local_multi_trade_signature_pair_diagnostic_list_samples;

View File

@@ -42,6 +42,7 @@ mod program_instruction_discriminator_summary;
mod wallet_participation;
pub(crate) use local_pipeline_diagnostics::LocalDecodedEventDiagnosticSummaryRow;
pub(crate) use local_pipeline_diagnostics::LocalEventClassificationDiagnosticSummaryRow;
pub(crate) use local_pipeline_diagnostics::LocalDexDiagnosticSummaryRow;
pub(crate) use local_pipeline_diagnostics::LocalDuplicateDecodedEventTradeDiagnosticSampleRow;
pub(crate) use local_pipeline_diagnostics::LocalMissingTradeEventDiagnosticSampleRow;
@@ -68,6 +69,7 @@ pub use launch_surface::LaunchSurfaceDto;
pub use launch_surface_key::LaunchSurfaceKeyDto;
pub use liquidity_event::LiquidityEventDto;
pub use local_pipeline_diagnostics::LocalDecodedEventDiagnosticSummaryDto;
pub use local_pipeline_diagnostics::LocalEventClassificationDiagnosticSummaryDto;
pub use local_pipeline_diagnostics::LocalDexDiagnosticSummaryDto;
pub use local_pipeline_diagnostics::LocalDuplicateDecodedEventTradeDiagnosticSampleDto;
pub use local_pipeline_diagnostics::LocalMissingTradeEventDiagnosticSampleDto;

View File

@@ -17,6 +17,12 @@ pub struct LocalPipelineDiagnosticSummaryDto {
pub decoded_trade_candidate_count: i64,
/// Total decoded DEX candle candidates.
pub decoded_candle_candidate_count: i64,
/// Total decoded useful non-trade events.
pub decoded_non_trade_useful_event_count: i64,
/// Total decoded swap-like events that are intentionally non-actionable.
pub decoded_non_actionable_trade_event_count: i64,
/// Total decoded events with unknown classification.
pub decoded_unknown_event_count: i64,
/// Whether the local persisted pipeline has no blocking diagnostic issue.
pub diagnostics_clean: bool,
/// Number of blocking diagnostic issues.
@@ -69,6 +75,9 @@ pub struct LocalPipelineDiagnosticSummaryDto {
pub pair_summaries: std::vec::Vec<crate::LocalPairDiagnosticSummaryDto>,
/// Diagnostics grouped by decoded event kind.
pub decoded_event_summaries: std::vec::Vec<crate::LocalDecodedEventDiagnosticSummaryDto>,
/// Diagnostics grouped by decoded event category, lifecycle kind and actionability.
pub event_classification_summaries:
std::vec::Vec<crate::LocalEventClassificationDiagnosticSummaryDto>,
/// Missing trade events grouped by diagnostic reason.
pub missing_trade_event_reason_summaries:
std::vec::Vec<crate::LocalMissingTradeEventReasonSummaryDto>,
@@ -157,6 +166,12 @@ pub struct LocalDecodedEventDiagnosticSummaryDto {
pub event_kind: std::string::String,
/// Event category.
pub event_category: std::option::Option<std::string::String>,
/// Event lifecycle kind.
pub event_lifecycle_kind: std::option::Option<std::string::String>,
/// Event actionability class.
pub event_actionability: std::option::Option<std::string::String>,
/// Whether payload says this event is a useful non-trade event.
pub non_trade_useful: std::option::Option<bool>,
/// Whether payload says this event is a trade candidate.
pub trade_candidate: std::option::Option<bool>,
/// Whether payload says this event is a candle candidate.
@@ -167,6 +182,27 @@ pub struct LocalDecodedEventDiagnosticSummaryDto {
pub trade_event_count: i64,
}
/// Local decoded-event classification summary.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LocalEventClassificationDiagnosticSummaryDto {
/// Event category.
pub event_category: std::string::String,
/// Event lifecycle kind.
pub event_lifecycle_kind: std::string::String,
/// Event actionability class.
pub event_actionability: std::string::String,
/// Whether payload says this event is a useful non-trade event.
pub non_trade_useful: bool,
/// Total decoded events in this classification group.
pub event_count: i64,
/// Total decoded trade candidates in this classification group.
pub decoded_trade_candidate_count: i64,
/// Total decoded candle candidates in this classification group.
pub decoded_candle_candidate_count: i64,
/// Total linked trade events in this classification group.
pub trade_event_count: i64,
}
/// Missing trade event diagnostics grouped by reason.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LocalMissingTradeEventReasonSummaryDto {
@@ -238,6 +274,12 @@ pub struct LocalPipelineDiagnosticCountersDto {
pub decoded_trade_candidate_count: i64,
/// Total decoded DEX candle candidates.
pub decoded_candle_candidate_count: i64,
/// Total decoded useful non-trade events.
pub decoded_non_trade_useful_event_count: i64,
/// Total decoded swap-like events that are intentionally non-actionable.
pub decoded_non_actionable_trade_event_count: i64,
/// Total decoded events with unknown classification.
pub decoded_unknown_event_count: i64,
/// Total decoded trade candidates without trade event, including ignored failed transactions.
pub missing_trade_event_count: i64,
/// Explicit alias for decoded trade candidates without linked trade event.
@@ -289,6 +331,9 @@ pub(crate) struct LocalPipelineDiagnosticCountersRow {
pub(crate) decoded_event_count: i64,
pub(crate) decoded_trade_candidate_count: i64,
pub(crate) decoded_candle_candidate_count: i64,
pub(crate) decoded_non_trade_useful_event_count: i64,
pub(crate) decoded_non_actionable_trade_event_count: i64,
pub(crate) decoded_unknown_event_count: i64,
pub(crate) missing_trade_event_count: i64,
pub(crate) decoded_trade_candidate_without_trade_event_count: i64,
pub(crate) decoded_trade_candidate_without_trade_event_on_ok_transaction_count: i64,
@@ -350,12 +395,28 @@ pub(crate) struct LocalDecodedEventDiagnosticSummaryRow {
pub(crate) protocol_name: std::string::String,
pub(crate) event_kind: std::string::String,
pub(crate) event_category: std::option::Option<std::string::String>,
pub(crate) event_lifecycle_kind: std::option::Option<std::string::String>,
pub(crate) event_actionability: std::option::Option<std::string::String>,
pub(crate) non_trade_useful: std::option::Option<i64>,
pub(crate) trade_candidate: std::option::Option<i64>,
pub(crate) candle_candidate: std::option::Option<i64>,
pub(crate) event_count: i64,
pub(crate) trade_event_count: i64,
}
/// SQL row for local decoded-event classification diagnostics.
#[derive(Debug, Clone, sqlx::FromRow)]
pub(crate) struct LocalEventClassificationDiagnosticSummaryRow {
pub(crate) event_category: std::string::String,
pub(crate) event_lifecycle_kind: std::string::String,
pub(crate) event_actionability: std::string::String,
pub(crate) non_trade_useful: i64,
pub(crate) event_count: i64,
pub(crate) decoded_trade_candidate_count: i64,
pub(crate) decoded_candle_candidate_count: i64,
pub(crate) trade_event_count: i64,
}
/// Sample of a decoded trade candidate without linked trade event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LocalMissingTradeEventDiagnosticSampleDto {

View File

@@ -83,6 +83,7 @@ pub use liquidity_event::query_liquidity_events_list_recent;
pub use liquidity_event::query_liquidity_events_upsert;
pub use local_pipeline_diagnostics::query_local_decoded_event_diagnostic_list_summaries;
pub use local_pipeline_diagnostics::query_local_duplicate_decoded_event_trade_diagnostic_list_samples;
pub use local_pipeline_diagnostics::query_local_event_classification_diagnostic_list_summaries;
pub use local_pipeline_diagnostics::query_local_missing_trade_event_diagnostic_list_samples;
pub use local_pipeline_diagnostics::query_local_missing_trade_event_reason_list_summaries;
pub use local_pipeline_diagnostics::query_local_multi_trade_signature_pair_diagnostic_list_samples;

View File

@@ -26,6 +26,28 @@ SELECT
FROM k_sol_dex_decoded_events
WHERE json_extract(payload_json, '$.candleCandidate') = 1
) AS decoded_candle_candidate_count,
(
SELECT COUNT(*)
FROM k_sol_dex_decoded_events
WHERE COALESCE(json_extract(payload_json, '$.nonTradeUseful'), 0) = 1
OR COALESCE(json_extract(payload_json, '$.eventActionability'), '') = 'non_trade_useful'
) AS decoded_non_trade_useful_event_count,
(
SELECT COUNT(*)
FROM k_sol_dex_decoded_events
WHERE COALESCE(json_extract(payload_json, '$.eventActionability'), '') = 'non_actionable_trade'
OR (
COALESCE(json_extract(payload_json, '$.eventActionability'), '') = ''
AND COALESCE(json_extract(payload_json, '$.eventCategory'), '') = 'trade'
AND COALESCE(json_extract(payload_json, '$.tradeCandidate'), 0) = 0
AND COALESCE(json_extract(payload_json, '$.transactionFailed'), 0) = 0
)
) AS decoded_non_actionable_trade_event_count,
(
SELECT COUNT(*)
FROM k_sol_dex_decoded_events
WHERE COALESCE(json_extract(payload_json, '$.eventCategory'), 'unknown') = 'unknown'
) AS decoded_unknown_event_count,
(
SELECT COUNT(*)
FROM k_sol_dex_decoded_events dde
@@ -250,6 +272,10 @@ SELECT
decoded_event_count: row.decoded_event_count,
decoded_trade_candidate_count: row.decoded_trade_candidate_count,
decoded_candle_candidate_count: row.decoded_candle_candidate_count,
decoded_non_trade_useful_event_count: row.decoded_non_trade_useful_event_count,
decoded_non_actionable_trade_event_count: row
.decoded_non_actionable_trade_event_count,
decoded_unknown_event_count: row.decoded_unknown_event_count,
missing_trade_event_count: row.missing_trade_event_count,
decoded_trade_candidate_without_trade_event_count: row
.decoded_trade_candidate_without_trade_event_count,
@@ -512,6 +538,9 @@ SELECT
dde.protocol_name AS protocol_name,
dde.event_kind AS event_kind,
json_extract(dde.payload_json, '$.eventCategory') AS event_category,
json_extract(dde.payload_json, '$.eventLifecycleKind') AS event_lifecycle_kind,
json_extract(dde.payload_json, '$.eventActionability') AS event_actionability,
json_extract(dde.payload_json, '$.nonTradeUseful') AS non_trade_useful,
json_extract(dde.payload_json, '$.tradeCandidate') AS trade_candidate,
json_extract(dde.payload_json, '$.candleCandidate') AS candle_candidate,
COUNT(dde.id) AS event_count,
@@ -522,6 +551,9 @@ GROUP BY
dde.protocol_name,
dde.event_kind,
event_category,
event_lifecycle_kind,
event_actionability,
non_trade_useful,
trade_candidate,
candle_candidate
ORDER BY
@@ -546,6 +578,9 @@ ORDER BY
protocol_name: row.protocol_name,
event_kind: row.event_kind,
event_category: row.event_category,
event_lifecycle_kind: row.event_lifecycle_kind,
event_actionability: row.event_actionability,
non_trade_useful: sqlite_bool_to_option(row.non_trade_useful),
trade_candidate: sqlite_bool_to_option(row.trade_candidate),
candle_candidate: sqlite_bool_to_option(row.candle_candidate),
event_count: row.event_count,
@@ -557,6 +592,68 @@ ORDER BY
}
}
/// Lists local decoded-event classification diagnostic summaries.
pub async fn query_local_event_classification_diagnostic_list_summaries(
database: &crate::Database,
) -> Result<std::vec::Vec<crate::LocalEventClassificationDiagnosticSummaryDto>, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let rows_result = sqlx::query_as::<
sqlx::Sqlite,
crate::db::dtos::LocalEventClassificationDiagnosticSummaryRow,
>(
r#"
SELECT
COALESCE(json_extract(dde.payload_json, '$.eventCategory'), 'unknown') AS event_category,
COALESCE(json_extract(dde.payload_json, '$.eventLifecycleKind'), 'unknown') AS event_lifecycle_kind,
COALESCE(json_extract(dde.payload_json, '$.eventActionability'), 'unknown') AS event_actionability,
CASE WHEN COALESCE(json_extract(dde.payload_json, '$.nonTradeUseful'), 0) = 1 THEN 1 ELSE 0 END AS non_trade_useful,
COUNT(dde.id) AS event_count,
COUNT(CASE WHEN COALESCE(json_extract(dde.payload_json, '$.tradeCandidate'), 0) = 1 THEN dde.id END) AS decoded_trade_candidate_count,
COUNT(CASE WHEN COALESCE(json_extract(dde.payload_json, '$.candleCandidate'), 0) = 1 THEN dde.id END) AS decoded_candle_candidate_count,
COUNT(te.id) AS trade_event_count
FROM k_sol_dex_decoded_events dde
LEFT JOIN k_sol_trade_events te ON te.decoded_event_id = dde.id
GROUP BY
event_category,
event_lifecycle_kind,
event_actionability,
non_trade_useful
ORDER BY
event_category,
event_lifecycle_kind,
event_actionability
"#,
)
.fetch_all(pool)
.await;
let rows = match rows_result {
Ok(rows) => rows,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list local decoded event classification summaries on sqlite: {}",
error
)));
},
};
let mut summaries = std::vec::Vec::new();
for row in rows {
summaries.push(crate::LocalEventClassificationDiagnosticSummaryDto {
event_category: row.event_category,
event_lifecycle_kind: row.event_lifecycle_kind,
event_actionability: row.event_actionability,
non_trade_useful: row.non_trade_useful != 0,
event_count: row.event_count,
decoded_trade_candidate_count: row.decoded_trade_candidate_count,
decoded_candle_candidate_count: row.decoded_candle_candidate_count,
trade_event_count: row.trade_event_count,
});
}
return Ok(summaries);
},
}
}
/// Lists missing trade events grouped by diagnostic reason.
pub async fn query_local_missing_trade_event_reason_list_summaries(
database: &crate::Database,

View File

@@ -160,6 +160,11 @@ fn prepare_payload_for_transaction_status(
},
};
object.insert("transactionFailed".to_string(), serde_json::Value::Bool(true));
object.insert(
"eventActionability".to_string(),
serde_json::Value::String("failed_transaction".to_string()),
);
object.insert("nonTradeUseful".to_string(), serde_json::Value::Bool(false));
object.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
object.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
object.insert(

View File

@@ -42,6 +42,95 @@ impl DexEventCategory {
}
}
/// Fine-grained lifecycle kind assigned to one decoded DEX event kind.
#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum DexEventLifecycleKind {
/// Swap-like trade event.
TradeSwap,
/// Pool creation or initialization event.
PoolCreation,
/// Pair creation event when it can be distinguished from pool creation.
PairCreation,
/// Liquidity deposit or add-liquidity event.
LiquidityAdd,
/// Liquidity withdraw or remove-liquidity event.
LiquidityRemove,
/// Concentrated-liquidity position open event.
PositionOpen,
/// Concentrated-liquidity position close event.
PositionClose,
/// Migration event, for example launch surface to AMM/CLMM/DLMM.
Migration,
/// Launch or bonding-curve initialization event.
Launch,
/// Token mint event detected through a DEX or launch surface decoder.
Mint,
/// Token burn event detected through a DEX or launch surface decoder.
Burn,
/// Fee collection event.
FeeCollection,
/// Reward or emission event.
Reward,
/// Administration, configuration or permission update event.
AdminConfig,
/// Event kind that is not classified yet.
Unknown,
}
impl DexEventLifecycleKind {
/// Returns the stable string code persisted inside decoded payload metadata.
pub fn as_str(self) -> &'static str {
match self {
Self::TradeSwap => return "trade_swap",
Self::PoolCreation => return "pool_creation",
Self::PairCreation => return "pair_creation",
Self::LiquidityAdd => return "liquidity_add",
Self::LiquidityRemove => return "liquidity_remove",
Self::PositionOpen => return "position_open",
Self::PositionClose => return "position_close",
Self::Migration => return "migration",
Self::Launch => return "launch",
Self::Mint => return "mint",
Self::Burn => return "burn",
Self::FeeCollection => return "fee_collection",
Self::Reward => return "reward",
Self::AdminConfig => return "admin_config",
Self::Unknown => return "unknown",
}
}
}
/// Stable actionability class assigned to one decoded DEX event.
#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum DexEventActionability {
/// Direct swap-like event that can feed trade/candle materialization.
TradeCandidate,
/// Swap-like event detected but not materializable as trade/candle yet.
NonActionableTrade,
/// Useful non-trade event that should remain visible for future materialization.
NonTradeUseful,
/// Failed transaction event retained for diagnostics but never actionable.
FailedTransaction,
/// Classified event that is informational only for the current pipeline.
Informational,
/// Event that is not classified yet.
Unknown,
}
impl DexEventActionability {
/// Returns the stable string code persisted inside decoded payload metadata.
pub fn as_str(self) -> &'static str {
match self {
Self::TradeCandidate => return "trade_candidate",
Self::NonActionableTrade => return "non_actionable_trade",
Self::NonTradeUseful => return "non_trade_useful",
Self::FailedTransaction => return "failed_transaction",
Self::Informational => return "informational",
Self::Unknown => return "unknown",
}
}
}
/// Classifies a DEX event kind into a stable business category.
pub fn classify_dex_event_category(event_kind: &str) -> DexEventCategory {
if is_dex_reward_event_kind(event_kind) {
@@ -70,6 +159,95 @@ pub fn classify_dex_event_category_code(event_kind: &str) -> &'static str {
return classify_dex_event_category(event_kind).as_str();
}
/// Classifies a DEX event kind into a fine-grained lifecycle kind.
pub fn classify_dex_event_lifecycle_kind(event_kind: &str) -> DexEventLifecycleKind {
if is_dex_token_burn_event_kind(event_kind) {
return DexEventLifecycleKind::Burn;
}
if is_dex_token_mint_event_kind(event_kind) {
return DexEventLifecycleKind::Mint;
}
if is_dex_migration_event_kind(event_kind) {
return DexEventLifecycleKind::Migration;
}
if is_dex_launch_event_kind(event_kind) {
return DexEventLifecycleKind::Launch;
}
if is_dex_pair_creation_event_kind(event_kind) {
return DexEventLifecycleKind::PairCreation;
}
if is_dex_pool_creation_event_kind(event_kind) {
return DexEventLifecycleKind::PoolCreation;
}
if is_dex_liquidity_add_event_kind(event_kind) {
return DexEventLifecycleKind::LiquidityAdd;
}
if is_dex_liquidity_remove_event_kind(event_kind) {
return DexEventLifecycleKind::LiquidityRemove;
}
if is_dex_position_open_event_kind(event_kind) {
return DexEventLifecycleKind::PositionOpen;
}
if is_dex_position_close_event_kind(event_kind) {
return DexEventLifecycleKind::PositionClose;
}
if is_dex_fee_event_kind(event_kind) {
return DexEventLifecycleKind::FeeCollection;
}
if is_dex_reward_event_kind(event_kind) {
return DexEventLifecycleKind::Reward;
}
if is_dex_admin_event_kind(event_kind) {
return DexEventLifecycleKind::AdminConfig;
}
if is_dex_trade_event_kind(event_kind) {
return DexEventLifecycleKind::TradeSwap;
}
return DexEventLifecycleKind::Unknown;
}
/// Classifies a DEX event kind and returns the persisted lifecycle kind code.
pub fn classify_dex_event_lifecycle_kind_code(event_kind: &str) -> &'static str {
return classify_dex_event_lifecycle_kind(event_kind).as_str();
}
/// Classifies one decoded DEX event actionability from its kind and candidate flags.
pub fn classify_dex_event_actionability(
event_kind: &str,
trade_candidate: bool,
transaction_failed: bool,
) -> DexEventActionability {
if transaction_failed {
return DexEventActionability::FailedTransaction;
}
if trade_candidate {
return DexEventActionability::TradeCandidate;
}
if is_dex_trade_event_kind(event_kind) {
return DexEventActionability::NonActionableTrade;
}
let category = classify_dex_event_category(event_kind);
match category {
DexEventCategory::Liquidity => return DexEventActionability::NonTradeUseful,
DexEventCategory::Fee => return DexEventActionability::NonTradeUseful,
DexEventCategory::Reward => return DexEventActionability::NonTradeUseful,
DexEventCategory::PoolLifecycle => return DexEventActionability::NonTradeUseful,
DexEventCategory::Admin => return DexEventActionability::NonTradeUseful,
DexEventCategory::Trade => return DexEventActionability::NonActionableTrade,
DexEventCategory::Unknown => return DexEventActionability::Unknown,
}
}
/// Classifies one decoded DEX event actionability and returns its persisted code.
pub fn classify_dex_event_actionability_code(
event_kind: &str,
trade_candidate: bool,
transaction_failed: bool,
) -> &'static str {
return classify_dex_event_actionability(event_kind, trade_candidate, transaction_failed)
.as_str();
}
/// Returns true when the event kind represents a swap-like event.
pub fn is_dex_trade_event_kind(event_kind: &str) -> bool {
if event_kind.ends_with(".buy") {
@@ -127,6 +305,50 @@ pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool {
return false;
}
/// Returns true for liquidity add-like DEX events.
pub fn is_dex_liquidity_add_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".deposit") {
return true;
}
if event_kind.contains(".add_liquidity") {
return true;
}
if event_kind.contains(".increase_liquidity") {
return true;
}
return false;
}
/// Returns true for liquidity remove-like DEX events.
pub fn is_dex_liquidity_remove_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".withdraw") {
return true;
}
if event_kind.contains(".remove_liquidity") {
return true;
}
if event_kind.contains(".decrease_liquidity") {
return true;
}
return false;
}
/// Returns true for concentrated-liquidity position open events.
pub fn is_dex_position_open_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".open_position") {
return true;
}
return false;
}
/// Returns true for concentrated-liquidity position close events.
pub fn is_dex_position_close_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".close_position") {
return true;
}
return false;
}
/// Returns true for fee collection events.
pub fn is_dex_fee_event_kind(event_kind: &str) -> bool {
if event_kind.contains("collect_creator_fee") {
@@ -155,8 +377,81 @@ pub fn is_dex_reward_event_kind(event_kind: &str) -> bool {
return false;
}
/// Returns true for pool creation, initialization or migration events.
/// Returns true for pool, pair, launch, mint, burn or migration lifecycle events.
pub fn is_dex_pool_lifecycle_event_kind(event_kind: &str) -> bool {
if is_dex_pool_creation_event_kind(event_kind) {
return true;
}
if is_dex_pair_creation_event_kind(event_kind) {
return true;
}
if is_dex_launch_event_kind(event_kind) {
return true;
}
if is_dex_token_mint_event_kind(event_kind) {
return true;
}
if is_dex_token_burn_event_kind(event_kind) {
return true;
}
if is_dex_migration_event_kind(event_kind) {
return true;
}
return false;
}
/// Returns true for launch or bonding-curve creation events.
pub fn is_dex_launch_event_kind(event_kind: &str) -> bool {
if event_kind.contains("pump_fun.create") {
return true;
}
if event_kind.contains(".launch") {
return true;
}
if event_kind.contains(".create_v2_token") {
return true;
}
if event_kind.contains(".create_bonding_curve") {
return true;
}
return false;
}
/// Returns true for token mint events detected by DEX or launch-surface decoders.
pub fn is_dex_token_mint_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".mint") {
return true;
}
if event_kind.contains(".token_mint") {
return true;
}
return false;
}
/// Returns true for token burn events detected by DEX or launch-surface decoders.
pub fn is_dex_token_burn_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".burn") {
return true;
}
if event_kind.contains(".token_burn") {
return true;
}
return false;
}
/// Returns true for launch-surface or pool migration events.
pub fn is_dex_migration_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".migrate") {
return true;
}
if event_kind.contains(".migration") {
return true;
}
return false;
}
/// Returns true for pool creation or initialization events.
pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".initialize") {
return true;
}
@@ -166,10 +461,18 @@ pub fn is_dex_pool_lifecycle_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".create_pool") {
return true;
}
if event_kind.contains(".create_v2_token") {
if event_kind.contains(".create_amm") {
return true;
}
if event_kind.contains(".migrate") {
return false;
}
/// Returns true for pair creation events when they are distinguishable from pool creation.
pub fn is_dex_pair_creation_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".create_pair") {
return true;
}
if event_kind.contains(".pair_create") {
return true;
}
return false;
@@ -264,8 +567,7 @@ pub fn enrich_dex_decoded_payload(
payload_json: serde_json::Value,
) -> serde_json::Value {
let event_category = classify_dex_event_category_code(event_kind);
let trade_candidate = is_dex_trade_event_kind(event_kind);
let candle_candidate = is_dex_candle_candidate_event_kind(event_kind);
let event_lifecycle_kind = classify_dex_event_lifecycle_kind_code(event_kind);
let mut object = match payload_json {
serde_json::Value::Object(object) => object,
other => {
@@ -274,14 +576,54 @@ pub fn enrich_dex_decoded_payload(
object
},
};
let payload_snapshot = serde_json::Value::Object(object.clone());
let explicit_trade_candidate = extract_top_level_bool_by_candidate_keys(
&payload_snapshot,
&["tradeCandidate", "trade_candidate"],
);
let trade_candidate = match explicit_trade_candidate {
Some(trade_candidate) => trade_candidate,
None => is_dex_trade_event_kind(event_kind),
};
let explicit_candle_candidate = extract_top_level_bool_by_candidate_keys(
&payload_snapshot,
&["candleCandidate", "candle_candidate"],
);
let candle_candidate = match explicit_candle_candidate {
Some(candle_candidate) => candle_candidate,
None => {
if !trade_candidate {
false
} else {
is_dex_candle_candidate_event_kind(event_kind)
}
},
};
let transaction_failed = match extract_top_level_bool_by_candidate_keys(
&payload_snapshot,
&["transactionFailed", "transaction_failed"],
) {
Some(transaction_failed) => transaction_failed,
None => false,
};
let event_actionability =
classify_dex_event_actionability_code(event_kind, trade_candidate, transaction_failed);
let non_trade_useful = event_actionability == DexEventActionability::NonTradeUseful.as_str();
json_insert_string_if_missing(&mut object, "protocolName", protocol_name);
json_insert_string_if_missing(&mut object, "eventKind", event_kind);
json_insert_string_if_missing(&mut object, "eventCategory", event_category);
json_insert_string_if_missing(&mut object, "eventLifecycleKind", event_lifecycle_kind);
json_insert_string_if_missing(&mut object, "eventActionability", event_actionability);
json_insert_bool_if_missing(&mut object, "nonTradeUseful", non_trade_useful);
json_insert_bool_if_missing(&mut object, "tradeCandidate", trade_candidate);
json_insert_bool_if_missing(&mut object, "candleCandidate", candle_candidate);
json_insert_i64_if_missing(&mut object, "eventClassificationVersion", 1);
json_insert_i64_if_missing(&mut object, "eventClassificationVersion", 2);
if !trade_candidate {
json_insert_string_if_missing(&mut object, "skipTradeReason", "non_trade_event");
if is_dex_trade_event_kind(event_kind) {
json_insert_string_if_missing(&mut object, "skipTradeReason", "non_actionable_trade");
} else {
json_insert_string_if_missing(&mut object, "skipTradeReason", "non_trade_event");
}
} else if !candle_candidate {
json_insert_string_if_missing(
&mut object,
@@ -525,6 +867,35 @@ mod tests {
);
}
#[test]
fn classifies_fine_grained_non_trade_lifecycle_kinds() {
assert_eq!(
super::classify_dex_event_lifecycle_kind_code("raydium_cpmm.initialize"),
"pool_creation"
);
assert_eq!(super::classify_dex_event_lifecycle_kind_code("pump_fun.create"), "launch");
assert_eq!(
super::classify_dex_event_lifecycle_kind_code("meteora_dbc.migrate"),
"migration"
);
assert_eq!(
super::classify_dex_event_lifecycle_kind_code("raydium_clmm.increase_liquidity_v2"),
"liquidity_add"
);
assert_eq!(
super::classify_dex_event_lifecycle_kind_code("raydium_clmm.decrease_liquidity_v2"),
"liquidity_remove"
);
assert_eq!(
super::classify_dex_event_actionability_code(
"raydium_clmm.increase_liquidity_v2",
false,
false,
),
"non_trade_useful"
);
}
#[test]
fn enriched_payload_keeps_existing_fields() {
let payload_json = serde_json::json!({
@@ -558,6 +929,45 @@ mod tests {
);
assert_eq!(object.get("tradeCandidate"), Some(&serde_json::Value::Bool(true)));
assert_eq!(object.get("candleCandidate"), Some(&serde_json::Value::Bool(true)));
assert_eq!(
object.get("eventLifecycleKind"),
Some(&serde_json::Value::String("trade_swap".to_owned()))
);
assert_eq!(
object.get("eventActionability"),
Some(&serde_json::Value::String("trade_candidate".to_owned()))
);
}
#[test]
fn enriched_non_trade_payload_is_visible_but_not_trade_candidate() {
let enriched_payload = super::enrich_dex_decoded_payload(
"raydium_clmm",
"raydium_clmm.increase_liquidity_v2",
serde_json::json!({}),
);
let object_option = enriched_payload.as_object();
let object = match object_option {
Some(object) => object,
None => {
panic!("expected enriched payload object");
},
};
assert_eq!(
object.get("eventCategory"),
Some(&serde_json::Value::String("liquidity".to_owned()))
);
assert_eq!(
object.get("eventLifecycleKind"),
Some(&serde_json::Value::String("liquidity_add".to_owned()))
);
assert_eq!(
object.get("eventActionability"),
Some(&serde_json::Value::String("non_trade_useful".to_owned()))
);
assert_eq!(object.get("nonTradeUseful"), Some(&serde_json::Value::Bool(true)));
assert_eq!(object.get("tradeCandidate"), Some(&serde_json::Value::Bool(false)));
assert_eq!(object.get("candleCandidate"), Some(&serde_json::Value::Bool(false)));
}
#[test]

View File

@@ -347,6 +347,8 @@ pub use db::LocalDecodedEventDiagnosticSummaryDto;
pub use db::LocalDexDiagnosticSummaryDto;
/// Sample of duplicated trade rows grouped by decoded event id.
pub use db::LocalDuplicateDecodedEventTradeDiagnosticSampleDto;
/// Local decoded-event classification diagnostics summary.
pub use db::LocalEventClassificationDiagnosticSummaryDto;
/// Sample of a decoded trade candidate without linked trade event.
pub use db::LocalMissingTradeEventDiagnosticSampleDto;
/// Missing trade event diagnostics grouped by reason.
@@ -559,6 +561,8 @@ pub use db::query_liquidity_events_upsert;
pub use db::query_local_decoded_event_diagnostic_list_summaries;
/// Lists samples of duplicated trade rows by decoded event id.
pub use db::query_local_duplicate_decoded_event_trade_diagnostic_list_samples;
/// Lists local decoded-event classification diagnostic summaries.
pub use db::query_local_event_classification_diagnostic_list_summaries;
/// Lists samples of decoded trade candidates without linked trade event.
pub use db::query_local_missing_trade_event_diagnostic_list_samples;
/// Lists missing trade events grouped by diagnostic reason.
@@ -831,12 +835,24 @@ pub use dex_decode::DexDecodeService;
pub use dex_detect::DexDetectService;
/// Result of one business-level DEX pool detection.
pub use dex_detect::DexPoolDetectionResult;
/// Stable DEX event actionability class.
pub use dex_event_classification::DexEventActionability;
/// Stable DEX event business category.
pub use dex_event_classification::DexEventCategory;
/// Fine-grained DEX event lifecycle kind.
pub use dex_event_classification::DexEventLifecycleKind;
/// Classifies a DEX event into an actionability class.
pub use dex_event_classification::classify_dex_event_actionability;
/// Classifies a DEX event into an actionability class and returns its persisted code.
pub use dex_event_classification::classify_dex_event_actionability_code;
/// Classifies a DEX event kind into a stable category.
pub use dex_event_classification::classify_dex_event_category;
/// Classifies a DEX event kind and returns its persisted category code.
pub use dex_event_classification::classify_dex_event_category_code;
/// Classifies a DEX event kind into a fine-grained lifecycle kind.
pub use dex_event_classification::classify_dex_event_lifecycle_kind;
/// Classifies a DEX event kind into a fine-grained lifecycle kind code.
pub use dex_event_classification::classify_dex_event_lifecycle_kind_code;
/// Enriches and serializes a decoded DEX payload.
pub use dex_event_classification::enrich_and_serialize_dex_decoded_payload;
/// Enriches a decoded DEX payload with classification metadata.
@@ -853,12 +869,32 @@ pub use dex_event_classification::is_dex_admin_event_kind;
pub use dex_event_classification::is_dex_candle_candidate_event_kind;
/// Returns true for fee collection DEX events.
pub use dex_event_classification::is_dex_fee_event_kind;
/// Returns true for launch or bonding-curve creation DEX events.
pub use dex_event_classification::is_dex_launch_event_kind;
/// Returns true for liquidity add-like DEX events.
pub use dex_event_classification::is_dex_liquidity_add_event_kind;
/// Returns true for liquidity lifecycle DEX events.
pub use dex_event_classification::is_dex_liquidity_event_kind;
/// Returns true for liquidity remove-like DEX events.
pub use dex_event_classification::is_dex_liquidity_remove_event_kind;
/// Returns true for migration DEX events.
pub use dex_event_classification::is_dex_migration_event_kind;
/// Returns true for pair creation DEX events.
pub use dex_event_classification::is_dex_pair_creation_event_kind;
/// Returns true for pool creation DEX events.
pub use dex_event_classification::is_dex_pool_creation_event_kind;
/// Returns true for pool lifecycle DEX events.
pub use dex_event_classification::is_dex_pool_lifecycle_event_kind;
/// Returns true for position close DEX events.
pub use dex_event_classification::is_dex_position_close_event_kind;
/// Returns true for position open DEX events.
pub use dex_event_classification::is_dex_position_open_event_kind;
/// Returns true for reward or emission DEX events.
pub use dex_event_classification::is_dex_reward_event_kind;
/// Returns true for token burn DEX events.
pub use dex_event_classification::is_dex_token_burn_event_kind;
/// Returns true for token mint DEX events.
pub use dex_event_classification::is_dex_token_mint_event_kind;
/// Returns true for swap-like DEX events.
pub use dex_event_classification::is_dex_trade_event_kind;
/// Static DEX support matrix entry.

View File

@@ -42,6 +42,15 @@ impl LocalPipelineDiagnosticsService {
Ok(decoded_event_summaries) => decoded_event_summaries,
Err(error) => return Err(error),
};
let event_classification_summaries_result =
crate::query_local_event_classification_diagnostic_list_summaries(
self.database.as_ref(),
)
.await;
let event_classification_summaries = match event_classification_summaries_result {
Ok(summaries) => summaries,
Err(error) => return Err(error),
};
let missing_trade_event_reason_summaries_result =
crate::query_local_missing_trade_event_reason_list_summaries(self.database.as_ref())
.await;
@@ -123,6 +132,10 @@ impl LocalPipelineDiagnosticsService {
decoded_event_count: counters.decoded_event_count,
decoded_trade_candidate_count: counters.decoded_trade_candidate_count,
decoded_candle_candidate_count: counters.decoded_candle_candidate_count,
decoded_non_trade_useful_event_count: counters.decoded_non_trade_useful_event_count,
decoded_non_actionable_trade_event_count: counters
.decoded_non_actionable_trade_event_count,
decoded_unknown_event_count: counters.decoded_unknown_event_count,
diagnostics_clean,
blocking_issue_count,
missing_trade_event_count: counters.missing_trade_event_count,
@@ -152,6 +165,7 @@ impl LocalPipelineDiagnosticsService {
dex_summaries,
pair_summaries,
decoded_event_summaries,
event_classification_summaries,
missing_trade_event_reason_summaries,
non_actionable_pair_count: counters.non_actionable_pair_count,
non_actionable_pair_summaries,

View File

@@ -64,7 +64,7 @@ impl LocalPipelineValidationConfig {
/// Builds the strict validation config for `0.7.27` non-regression runs.
pub fn v0_7_27_multi_dex_non_regression() -> Self {
return Self {
profile_code: "0.7.27_multi_dex_non_regression (obsolete)".to_string(),
profile_code: "0.7.27_multi_dex_non_regression".to_string(),
expected_dex_codes: vec![
"pump_fun".to_string(),
"pump_swap".to_string(),
@@ -146,6 +146,17 @@ impl LocalPipelineValidationConfig {
require_candles_per_dex: false,
};
}
/// Builds the `0.7.30` non-trade event classification validation config.
///
/// This profile keeps the `0.7.29` trade/candle checks and exposes the new
/// decoded-event classification counters. Non-trade events are intentionally
/// observable but not blocking until their dedicated materializers are added.
pub fn v0_7_30_non_trade_event_classification() -> Self {
let mut config = Self::v0_7_29_multi_dex_matrix_baseline();
config.profile_code = "0.7.30_non_trade_event_classification".to_string();
return config;
}
}
/// A single local pipeline validation issue.
@@ -178,6 +189,12 @@ pub struct LocalPipelineValidationReportDto {
pub expected_dex_codes: std::vec::Vec<std::string::String>,
/// Observed DEX codes found in diagnostics.
pub observed_dex_codes: std::vec::Vec<std::string::String>,
/// Total decoded useful non-trade events.
pub decoded_non_trade_useful_event_count: i64,
/// Total decoded swap-like events that are intentionally non-actionable.
pub decoded_non_actionable_trade_event_count: i64,
/// Total decoded events with unknown classification.
pub decoded_unknown_event_count: i64,
/// Number of entries currently exposed by the DEX support matrix.
pub dex_support_matrix_entry_count: i64,
/// DEX support matrix snapshot exposed with the validation report.
@@ -268,6 +285,14 @@ impl LocalPipelineValidationService {
let config = crate::LocalPipelineValidationConfig::v0_7_29_multi_dex_matrix_baseline();
return self.validate_current_database(&config).await;
}
/// Diagnoses the current database with the `0.7.30` non-trade classification profile.
pub async fn validate_v0_7_30_current_database(
&self,
) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
let config = crate::LocalPipelineValidationConfig::v0_7_30_non_trade_event_classification();
return self.validate_current_database(&config).await;
}
}
/// Validates a diagnostics summary without performing database access.
@@ -427,6 +452,10 @@ pub fn validate_local_pipeline_diagnostics_summary(
warning_count,
expected_dex_codes,
observed_dex_codes,
decoded_non_trade_useful_event_count: summary.decoded_non_trade_useful_event_count,
decoded_non_actionable_trade_event_count: summary
.decoded_non_actionable_trade_event_count,
decoded_unknown_event_count: summary.decoded_unknown_event_count,
dex_support_matrix_entry_count: crate::dex_support_matrix_entries().len() as i64,
dex_support_matrix: crate::dex_support_matrix_entry_dtos(),
issues,
@@ -470,6 +499,9 @@ mod tests {
decoded_event_count: 216,
decoded_trade_candidate_count: 216,
decoded_candle_candidate_count: 216,
decoded_non_trade_useful_event_count: 0,
decoded_non_actionable_trade_event_count: 0,
decoded_unknown_event_count: 0,
diagnostics_clean: true,
blocking_issue_count: 0,
missing_trade_event_count: 6,
@@ -536,6 +568,7 @@ mod tests {
],
pair_summaries: vec![],
decoded_event_summaries: vec![],
event_classification_summaries: vec![],
missing_trade_event_reason_summaries: vec![],
non_actionable_pair_summaries: vec![],
missing_trade_event_samples: vec![],
@@ -610,6 +643,33 @@ mod tests {
assert!(report.expected_dex_codes.contains(&"meteora_damm_v1".to_string()));
}
#[test]
fn validation_accepts_0_7_30_non_trade_classification_summary() {
let mut summary = make_0_7_28_summary_with_meteora();
summary.decoded_non_trade_useful_event_count = 3;
summary.decoded_non_actionable_trade_event_count = 1;
summary.decoded_unknown_event_count = 0;
summary.event_classification_summaries.push(
crate::LocalEventClassificationDiagnosticSummaryDto {
event_category: "pool_lifecycle".to_string(),
event_lifecycle_kind: "pool_creation".to_string(),
event_actionability: "non_trade_useful".to_string(),
non_trade_useful: true,
event_count: 3,
decoded_trade_candidate_count: 0,
decoded_candle_candidate_count: 0,
trade_event_count: 0,
},
);
let config = crate::LocalPipelineValidationConfig::v0_7_30_non_trade_event_classification();
let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
assert!(report.validation_passed);
assert_eq!(report.validation_profile_code, "0.7.30_non_trade_event_classification");
assert_eq!(report.decoded_non_trade_useful_event_count, 3);
assert_eq!(report.decoded_non_actionable_trade_event_count, 1);
assert_eq!(report.decoded_unknown_event_count, 0);
}
#[test]
fn validation_report_exposes_dex_support_matrix() {
let summary = make_0_7_28_summary_with_meteora();