diff --git a/CHANGELOG.md b/CHANGELOG.md
index be3ffad..0f697fb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -62,3 +62,4 @@
0.7.29 - Ajout d’une matrice DEX commune (`dex_support_matrix`) utilisée par le catalogue DEX, la classification transactionnelle et l’enregistrement des protocol candidates ; ajout du profil de validation `0.7.29_multi_dex_matrix_baseline` exposant la matrice dans le rapport de validation ; préparation explicite des surfaces planifiées sans inventer de program ids non vérifiés.
0.7.30 - Ajout d’une taxonomie DEX plus fine pour les événements décodés : `eventLifecycleKind`, `eventActionability`, `nonTradeUseful`, compteurs diagnostics des événements non-trade utiles, trades non actionnables et classifications inconnues ; ajout du profil `0.7.30_non_trade_event_classification` sans modification volontaire de la matérialisation trade/candle.
0.7.31 - Application de la politique Option B : les transactions failed restent traçables dans les événements décodés mais ne peuvent plus alimenter `trade_events`, metrics ou candles ; le replay local réinitialise les tables de matérialisation marché avant reconstruction pour supprimer les anciennes lignes dérivées non actionnables.
+0.7.32 - Clarification de la sémantique des diagnostics locaux : séparation des gaps littéraux de paires et des gaps bloquants/actionnables, ajout des compteurs de matérialisation par paire, résumé `pairActionabilitySummaries`, profil `0.7.32_validation_report_semantics` et garde-fous sur la matrice DEX sans modification de la matérialisation trade/candle.
diff --git a/Cargo.toml b/Cargo.toml
index f094054..4137221 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
-version = "0.7.31"
+version = "0.7.32"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot"
diff --git a/README.md b/README.md
index 12a55fe..6caea34 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
`khadhroony-bobobot` est un workspace Rust destiné à la détection, au décodage, à l’analyse et, à terme, au trading semi-automatisé de tokens Solana.
-Le README précédent décrivait surtout l’état `0.3.1`. Ce fichier reflète l’état de reprise autour de `0.7.30` : le socle transport HTTP/WS, la résolution transactionnelle, le modèle SQLite, plusieurs connecteurs DEX, les candles, les signaux analytiques, la validation locale et une matrice DEX commune existent déjà.
+Le README précédent décrivait surtout l’état `0.3.1`. Ce fichier reflète l’état de reprise autour de `0.7.32` : le socle transport HTTP/WS, la résolution transactionnelle, le modèle SQLite, plusieurs connecteurs DEX, les candles, les signaux analytiques, la validation locale et une matrice DEX commune existent déjà.
## 1. Objectif
@@ -31,7 +31,7 @@ Le workspace contient deux crates principales.
La logique métier doit rester dans `kb_lib`. `kb_demo_app` doit rester une façade UI/Tauri et ne doit pas récupérer de logique Solana ou DEX profonde.
-## 3. État actuel autour de `0.7.30`
+## 3. État actuel autour de `0.7.32`
### 3.1. Socle stabilisé à ne pas refactorer maintenant
@@ -99,6 +99,8 @@ Depuis `0.7.29`, la matrice de support DEX est portée par `kb_lib/src/dex_suppo
Depuis `0.7.30`, les événements décodés reçoivent aussi une classification plus fine : `eventLifecycleKind`, `eventActionability` et `nonTradeUseful`. Cette classification sert aux diagnostics et prépare la matérialisation future des événements non-trade sans alimenter directement les trades/candles.
+Depuis `0.7.32`, les diagnostics distinguent explicitement les gaps littéraux de catalogue (`literalPairWithoutTradeCount`, `literalPairWithoutCandleCount`) des gaps bloquants/actionnables (`blockingPairWithoutTradeCount`, `blockingPairWithoutCandleCount`). Les anciens champs `pairWithoutTradeCount` et `pairWithoutCandleCount` restent exposés comme alias de compatibilité pour les gaps bloquants/actionnables.
+
| Code cible | Type | Statut `0.7.29` | Prochaine action |
|---|---:|---|---|
| `pump_fun` | Launch + bonding curve | partiel | verrouiller le rattachement mint initial -> pools migrés |
@@ -215,9 +217,10 @@ Les tests peuvent rester plus souples lorsque cela clarifie le test.
La reprise doit suivre cet ordre :
-1. conserver la non-régression `0.7.31` : transactions failed traçables mais exclues des `trade_events`, metrics et candles ;
-2. utiliser la matrice `0.7.29` comme source commune pour le catalogue, la classification et les protocol candidates ;
-3. relier progressivement les événements non-trade aux tables existantes : lifecycle, liquidité, fees, rewards, admin ;
+1. conserver la sémantique `0.7.32` : les gaps littéraux de catalogue ne doivent pas être confondus avec les gaps bloquants/actionnables utilisés par la validation ;
+2. conserver la non-régression `0.7.31` : transactions failed traçables mais exclues des `trade_events`, metrics et candles ;
+3. utiliser la matrice `0.7.29` comme source commune pour le catalogue, la classification et les protocol candidates ;
+4. relier progressivement les événements non-trade aux tables existantes : lifecycle, liquidité, fees, rewards, admin ;
4. consolider Meteora, surtout `meteora_dlmm` et le cas partiel `meteora_damm_v1` ;
5. ajouter les launch surfaces manquantes comme origines de mint : LaunchLab/Launchpad, LetsBonk/Bonk.fun, Boop.fun, Moonshot/Moonit, Believe, Bags ;
6. traiter Heaven ;
diff --git a/ROADMAP.md b/ROADMAP.md
index 4fbcbcf..3063133 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -846,18 +846,20 @@ Réalisé :
- exposer `resetMarketMaterializationDeletedCount` dans le résultat de replay UI ;
- conserver la validation multi-DEX et la matrice DEX comme garde-fous avant d’ajouter les surfaces restantes.
-### 6.064. Version `0.7.32` — Transactions inconnues et protocol candidates
-Objectif : ne plus perdre les transactions utiles qui ne correspondent pas encore à un DEX connu.
+### 6.064. Version `0.7.32` — Sémantique des diagnostics et compteurs de validation
+Réalisé :
-À faire :
+- conserver la politique `0.7.31` : transactions failed traçables mais exclues des `trade_events`, metrics et candles ;
+- clarifier que `pairWithoutTradeCount` et `pairWithoutCandleCount` sont des compteurs de gaps bloquants/actionnables, pas des compteurs littéraux sur tout le catalogue ;
+- ajouter `literalPairWithoutTradeCount` et `literalPairWithoutCandleCount` pour les paires de catalogue sans trade/candle matérialisé ;
+- ajouter `blockingPairWithoutTradeCount` et `blockingPairWithoutCandleCount` comme noms explicites des anciens compteurs bloquants ;
+- ajouter les compteurs de matérialisation par paire : `tradeMaterializedPairCount`, `candleMaterializedPairCount`, `actionablePairCount`, `candleBucketTimeframeCount` et `candlesAreBucketed` ;
+- ajouter `pairActionabilitySummaries` pour distinguer les paires matérialisées, actionnables sans matérialisation, candidates failed, non-actionables, décodées sans trade candidate et catalog-only ;
+- ajouter le profil `0.7.32_validation_report_semantics` ;
+- ajouter des garde-fous de validation sur la matrice DEX : entrées `supported` entièrement matérialisées, entrées `partial` avec `skipReason`, entrées `planned/to_verify` non activées au catalogue ;
+- ne pas modifier la logique de replay, trade aggregation ou candle aggregation validée en `0.7.31`.
-- consolider `k_sol_transaction_classifications`, déjà présente, avec les catégories utiles au suivi DEX,
-- consolider `k_sol_protocol_candidates`, déjà présente, pour prioriser les programmes inconnus ou partiellement reconnus,
-- classifier les transactions résolues en catégories : known supported, known partial, known non-trade, unknown program, unknown protocol candidate, unknown event kind, failed transaction, non-actionable trade,
-- conserver les `program_id`, comptes, signatures, préfixes de `data`, logs et indices d’instructions utiles à l’analyse,
-- créer des requêtes de diagnostic pour repérer les programmes inconnus fréquents,
-- permettre de promouvoir plus tard un protocol candidate vers un vrai DEX/surface sans perdre l’historique,
-- garantir que ces tables n’alimentent jamais directement les trades/candles.
+Repoussé après cette clarification : consolider les transactions inconnues et protocol candidates sans polluer les trades/candles.
### 6.065. Version `0.7.33` — Événements non-trade v1 : liquidité et cycle de vie pool
Objectif : exploiter les événements utiles à l’analyse et au trading semi-automatique sans les mélanger avec les swaps/candles.
@@ -1212,17 +1214,18 @@ Le projet doit maintenir au minimum :
La priorité immédiate est désormais la suivante :
1. conserver la validation acquise `0.7.31` : transactions failed traçables mais exclues des `trade_events`, metrics et candles, aucun trade/candle candidate sans payload montant/prix exploitable, aucun diagnostic bloquant masqué,
-2. utiliser la matrice `0.7.29` (`kb_lib/src/dex_support_matrix.rs`) comme source commune pour le catalogue DEX, les mappings program id -> protocole, la classification transactionnelle et les protocol candidates,
-3. garder les clients HTTP/WS et managers réseau hors du refactor DEX tant qu’ils ne bloquent pas le pipeline,
-4. consolider les événements non-trade sans les confondre avec les trades/candles : lifecycle de pool, liquidité, fees, rewards, admin/config, migration et launch/mint,
-5. rattacher les launch surfaces aux tokens et aux pools migrés : Raydium LaunchLab/Launchpad, LetsBonk/Bonk.fun, Boop.fun, Moonshot/Moonit, Believe, Bags et Heaven,
-6. consolider Meteora avec corpus fiable : `meteora_dlmm`, `meteora_damm_v1`, `meteora_damm_v2`, `meteora_dbc` et `meteora_dlc` si le programme est confirmé,
-7. consolider Orca, FluxBeam et DexLab sur corpus,
-8. traiter `raydium_amm_v4` legacy seulement après les autres Raydium, avec corpus dédié prouvant le programme `675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8`,
-9. ajouter une matérialisation dédiée des transactions inconnues ou partiellement décodées pour analyser les DEX manquants sans polluer les trades/candles,
-10. effectuer une validation DEX v1 consolidée sur tous les connecteurs supportés avant de considérer la couche DEX `0.7.x` comme stable,
-11. ajouter ensuite les overlays des signaux analytiques sur les candles,
-12. consolider les vues métier `token / pair / pool` dans `kb_demo_app`, y compris les événements liquidité, lifecycle, fees, rewards et admin,
-13. stabiliser l’ergonomie, les filtres, la pagination et la navigation de l’UI d’inspection,
-14. préparer ensuite l’ouverture de `0.8.x` pour l’analyse, les filtres, les patterns et les projections graphiques,
-15. préparer enfin Yellowstone gRPC comme extension de capacité, et non comme remplacement du socle HTTP / WS existant.
+2. conserver la clarification `0.7.32` entre gaps littéraux de catalogue et gaps bloquants/actionnables,
+3. utiliser la matrice `0.7.29` (`kb_lib/src/dex_support_matrix.rs`) comme source commune pour le catalogue DEX, les mappings program id -> protocole, la classification transactionnelle et les protocol candidates,
+4. garder les clients HTTP/WS et managers réseau hors du refactor DEX tant qu’ils ne bloquent pas le pipeline,
+5. consolider les événements non-trade sans les confondre avec les trades/candles : lifecycle de pool, liquidité, fees, rewards, admin/config, migration et launch/mint,
+6. rattacher les launch surfaces aux tokens et aux pools migrés : Raydium LaunchLab/Launchpad, LetsBonk/Bonk.fun, Boop.fun, Moonshot/Moonit, Believe, Bags et Heaven,
+7. consolider Meteora avec corpus fiable : `meteora_dlmm`, `meteora_damm_v1`, `meteora_damm_v2`, `meteora_dbc` et `meteora_dlc` si le programme est confirmé,
+8. consolider Orca, FluxBeam et DexLab sur corpus,
+9. traiter `raydium_amm_v4` legacy seulement après les autres Raydium, avec corpus dédié prouvant le programme `675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8`,
+10. ajouter une matérialisation dédiée des transactions inconnues ou partiellement décodées pour analyser les DEX manquants sans polluer les trades/candles,
+11. effectuer une validation DEX v1 consolidée sur tous les connecteurs supportés avant de considérer la couche DEX `0.7.x` comme stable,
+12. ajouter ensuite les overlays des signaux analytiques sur les candles,
+13. consolider les vues métier `token / pair / pool` dans `kb_demo_app`, y compris les événements liquidité, lifecycle, fees, rewards et admin,
+14. stabiliser l’ergonomie, les filtres, la pagination et la navigation de l’UI d’inspection,
+15. préparer ensuite l’ouverture de `0.8.x` pour l’analyse, les filtres, les patterns et les projections graphiques,
+16. préparer enfin Yellowstone gRPC comme extension de capacité, et non comme remplacement du socle HTTP / WS existant.
diff --git a/kb_demo_app/frontend/demo_pipeline2.html b/kb_demo_app/frontend/demo_pipeline2.html
index 7bf1936..e5f1227 100644
--- a/kb_demo_app/frontend/demo_pipeline2.html
+++ b/kb_demo_app/frontend/demo_pipeline2.html
@@ -166,7 +166,8 @@
Validation profile
- 0.7.31 — trade event actionability policy
+ 0.7.32 — validation report semantics
+ 0.7.31 — trade event actionability policy
0.7.30 — non-trade event classification
0.7.29 — DEX matrix baseline
0.7.28 — multi-DEX non-regression
diff --git a/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPairActionabilityDiagnosticSummary.ts b/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPairActionabilityDiagnosticSummary.ts
new file mode 100644
index 0000000..79651bc
--- /dev/null
+++ b/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPairActionabilityDiagnosticSummary.ts
@@ -0,0 +1,38 @@
+// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
+
+/**
+ * Local pair actionability diagnostics summary for the UI.
+ */
+export type DemoPipeline2LocalPairActionabilityDiagnosticSummary = {
+/**
+ * Pair actionability or materialization class.
+ */
+pairActionability: string,
+/**
+ * Pair count.
+ */
+pairCount: number,
+/**
+ * Decoded event count.
+ */
+decodedEventCount: number,
+/**
+ * Decoded trade candidate count.
+ */
+decodedTradeCandidateCount: number,
+/**
+ * Actionable trade candidate count.
+ */
+actionableTradeCandidateCount: number,
+/**
+ * Failed trade candidate count.
+ */
+failedTradeCandidateCount: number,
+/**
+ * Trade event count.
+ */
+tradeEventCount: number,
+/**
+ * Pair candle count.
+ */
+pairCandleCount: number, };
diff --git a/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPipelineDiagnosticSummary.ts b/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPipelineDiagnosticSummary.ts
index 7c399bc..c40822e 100644
--- a/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPipelineDiagnosticSummary.ts
+++ b/kb_demo_app/frontend/ts/bindings/DemoPipeline2LocalPipelineDiagnosticSummary.ts
@@ -7,6 +7,7 @@ import type { DemoPipeline2LocalMissingTradeEventDiagnosticSample } from "./Demo
import type { DemoPipeline2LocalMissingTradeEventReasonSummary } from "./DemoPipeline2LocalMissingTradeEventReasonSummary";
import type { DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample } from "./DemoPipeline2LocalMultiTradeSignaturePairDiagnosticSample";
import type { DemoPipeline2LocalNonActionablePairDiagnosticSummary } from "./DemoPipeline2LocalNonActionablePairDiagnosticSummary";
+import type { DemoPipeline2LocalPairActionabilityDiagnosticSummary } from "./DemoPipeline2LocalPairActionabilityDiagnosticSummary";
import type { DemoPipeline2LocalPairDiagnosticSummary } from "./DemoPipeline2LocalPairDiagnosticSummary";
import type { DemoPipeline2LocalPairGapDiagnosticSample } from "./DemoPipeline2LocalPairGapDiagnosticSample";
@@ -124,11 +125,51 @@ poolCount: number,
*/
pairCount: number,
/**
- * Total pairs without trade.
+ * Stable explanation for legacy pair gap counters.
+ */
+pairGapCounterSemantics: string,
+/**
+ * Total pairs without any persisted trade event.
+ */
+literalPairWithoutTradeCount: number,
+/**
+ * Total pairs without any persisted candle.
+ */
+literalPairWithoutCandleCount: number,
+/**
+ * Total pairs that have at least one persisted trade event.
+ */
+tradeMaterializedPairCount: number,
+/**
+ * Total pairs that have at least one persisted candle bucket.
+ */
+candleMaterializedPairCount: number,
+/**
+ * Total pairs with at least one successful decoded trade candidate.
+ */
+actionablePairCount: number,
+/**
+ * Total distinct candle timeframes currently materialized.
+ */
+candleBucketTimeframeCount: number,
+/**
+ * Whether candle rows are bucketed aggregates rather than one row per trade.
+ */
+candlesAreBucketed: boolean,
+/**
+ * Total pairs without trade among actionable successful trade candidates.
+ */
+blockingPairWithoutTradeCount: number,
+/**
+ * Total pairs without candle among actionable successful candle candidates.
+ */
+blockingPairWithoutCandleCount: number,
+/**
+ * Total pairs without trade. Legacy alias for blocking/actionable gaps.
*/
pairWithoutTradeCount: number,
/**
- * Total pairs without candle.
+ * Total pairs without candle. Legacy alias for blocking/actionable gaps.
*/
pairWithoutCandleCount: number,
/**
@@ -139,6 +180,10 @@ dexSummaries: Array,
* Diagnostics grouped by pair.
*/
pairSummaries: Array,
+/**
+ * Diagnostics grouped by pair materialization/actionability class.
+ */
+pairActionabilitySummaries: Array,
/**
* Diagnostics grouped by decoded event kind.
*/
diff --git a/kb_demo_app/package.json b/kb_demo_app/package.json
index 9cc9dd5..a8d17be 100644
--- a/kb_demo_app/package.json
+++ b/kb_demo_app/package.json
@@ -1,7 +1,7 @@
{
"name": "kb-demo-app",
"private": true,
- "version": "0.7.31",
+ "version": "0.7.32",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/kb_demo_app/src/demo_pipeline2.rs b/kb_demo_app/src/demo_pipeline2.rs
index 1ad418b..ec0e955 100644
--- a/kb_demo_app/src/demo_pipeline2.rs
+++ b/kb_demo_app/src/demo_pipeline2.rs
@@ -327,16 +327,47 @@ pub(crate) struct DemoPipeline2LocalPipelineDiagnosticSummary {
/// Total known pairs.
#[ts(type = "number")]
pub pair_count: i64,
- /// Total pairs without trade.
+ /// Stable explanation for legacy pair gap counters.
+ pub pair_gap_counter_semantics: std::string::String,
+ /// Total pairs without any persisted trade event.
+ #[ts(type = "number")]
+ pub literal_pair_without_trade_count: i64,
+ /// Total pairs without any persisted candle.
+ #[ts(type = "number")]
+ pub literal_pair_without_candle_count: i64,
+ /// Total pairs that have at least one persisted trade event.
+ #[ts(type = "number")]
+ pub trade_materialized_pair_count: i64,
+ /// Total pairs that have at least one persisted candle bucket.
+ #[ts(type = "number")]
+ pub candle_materialized_pair_count: i64,
+ /// Total pairs with at least one successful decoded trade candidate.
+ #[ts(type = "number")]
+ pub actionable_pair_count: i64,
+ /// Total distinct candle timeframes currently materialized.
+ #[ts(type = "number")]
+ pub candle_bucket_timeframe_count: i64,
+ /// Whether candle rows are bucketed aggregates rather than one row per trade.
+ pub candles_are_bucketed: bool,
+ /// Total pairs without trade among actionable successful trade candidates.
+ #[ts(type = "number")]
+ pub blocking_pair_without_trade_count: i64,
+ /// Total pairs without candle among actionable successful candle candidates.
+ #[ts(type = "number")]
+ pub blocking_pair_without_candle_count: i64,
+ /// Total pairs without trade. Legacy alias for blocking/actionable gaps.
#[ts(type = "number")]
pub pair_without_trade_count: i64,
- /// Total pairs without candle.
+ /// Total pairs without candle. Legacy alias for blocking/actionable gaps.
#[ts(type = "number")]
pub pair_without_candle_count: i64,
/// Diagnostics grouped by DEX.
pub dex_summaries: std::vec::Vec,
/// Diagnostics grouped by pair.
pub pair_summaries: std::vec::Vec,
+ /// Diagnostics grouped by pair materialization/actionability class.
+ pub pair_actionability_summaries:
+ std::vec::Vec,
/// Diagnostics grouped by decoded event kind.
pub decoded_event_summaries: std::vec::Vec,
/// Diagnostics grouped by decoded event classification.
@@ -447,6 +478,39 @@ pub(crate) struct DemoPipeline2LocalPairDiagnosticSummary {
pub last_price_quote_per_base: std::option::Option,
}
+/// Local pair actionability diagnostics summary for the UI.
+#[derive(Clone, Debug, serde::Serialize, TS)]
+#[ts(
+ export,
+ export_to = "../frontend/ts/bindings/DemoPipeline2LocalPairActionabilityDiagnosticSummary.ts"
+)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct DemoPipeline2LocalPairActionabilityDiagnosticSummary {
+ /// Pair actionability or materialization class.
+ pub pair_actionability: std::string::String,
+ /// Pair count.
+ #[ts(type = "number")]
+ pub pair_count: i64,
+ /// Decoded event count.
+ #[ts(type = "number")]
+ pub decoded_event_count: i64,
+ /// Decoded trade candidate count.
+ #[ts(type = "number")]
+ pub decoded_trade_candidate_count: i64,
+ /// Actionable trade candidate count.
+ #[ts(type = "number")]
+ pub actionable_trade_candidate_count: i64,
+ /// Failed trade candidate count.
+ #[ts(type = "number")]
+ pub failed_trade_candidate_count: i64,
+ /// Trade event count.
+ #[ts(type = "number")]
+ pub trade_event_count: i64,
+ /// Pair candle count.
+ #[ts(type = "number")]
+ pub pair_candle_count: i64,
+}
+
/// Local decoded-event diagnostics summary for the UI.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[ts(
@@ -971,7 +1035,7 @@ pub(crate) async fn demo_pipeline2_validate_local_pipeline(
let service = kb_lib::LocalPipelineValidationService::new(database.clone());
let profile_code = match request {
Some(request) => request.profile_code,
- None => "0.7.31_trade_event_actionability_policy".to_string(),
+ None => "0.7.32_validation_report_semantics".to_string(),
};
let run_result = match profile_code.as_str() {
"0.7.27" | "0.7.27_dexes_non_regression" => {
@@ -989,6 +1053,9 @@ pub(crate) async fn demo_pipeline2_validate_local_pipeline(
"0.7.31" | "0.7.31_trade_event_actionability_policy" => {
service.validate_v0_7_31_current_database().await
},
+ "0.7.32" | "0.7.32_validation_report_semantics" => {
+ service.validate_v0_7_32_current_database().await
+ },
other => Err(kb_lib::Error::InvalidState(format!(
"unsupported local pipeline validation profile: {other}"
))),
@@ -1472,6 +1539,14 @@ fn demo_pipeline2_map_local_diagnostics_summary(
for pair_summary in summary.pair_summaries {
pair_summaries.push(demo_pipeline2_map_local_pair_diagnostic_summary(pair_summary));
}
+ let mut pair_actionability_summaries = std::vec::Vec::new();
+ for pair_actionability_summary in summary.pair_actionability_summaries {
+ pair_actionability_summaries.push(
+ demo_pipeline2_map_local_pair_actionability_diagnostic_summary(
+ pair_actionability_summary,
+ ),
+ );
+ }
let mut decoded_event_summaries = std::vec::Vec::new();
for decoded_event_summary in summary.decoded_event_summaries {
decoded_event_summaries
@@ -1552,10 +1627,21 @@ fn demo_pipeline2_map_local_diagnostics_summary(
token_metadata_missing_count: summary.token_metadata_missing_count,
pool_count: summary.pool_count,
pair_count: summary.pair_count,
+ pair_gap_counter_semantics: summary.pair_gap_counter_semantics,
+ literal_pair_without_trade_count: summary.literal_pair_without_trade_count,
+ literal_pair_without_candle_count: summary.literal_pair_without_candle_count,
+ trade_materialized_pair_count: summary.trade_materialized_pair_count,
+ candle_materialized_pair_count: summary.candle_materialized_pair_count,
+ actionable_pair_count: summary.actionable_pair_count,
+ candle_bucket_timeframe_count: summary.candle_bucket_timeframe_count,
+ candles_are_bucketed: summary.candles_are_bucketed,
+ blocking_pair_without_trade_count: summary.blocking_pair_without_trade_count,
+ blocking_pair_without_candle_count: summary.blocking_pair_without_candle_count,
pair_without_trade_count: summary.pair_without_trade_count,
pair_without_candle_count: summary.pair_without_candle_count,
dex_summaries,
pair_summaries,
+ pair_actionability_summaries,
decoded_event_summaries,
event_classification_summaries,
missing_trade_event_reason_summaries,
@@ -1606,6 +1692,21 @@ fn demo_pipeline2_map_local_pair_diagnostic_summary(
}
}
+fn demo_pipeline2_map_local_pair_actionability_diagnostic_summary(
+ summary: kb_lib::LocalPairActionabilityDiagnosticSummaryDto,
+) -> DemoPipeline2LocalPairActionabilityDiagnosticSummary {
+ DemoPipeline2LocalPairActionabilityDiagnosticSummary {
+ pair_actionability: summary.pair_actionability,
+ pair_count: summary.pair_count,
+ decoded_event_count: summary.decoded_event_count,
+ decoded_trade_candidate_count: summary.decoded_trade_candidate_count,
+ actionable_trade_candidate_count: summary.actionable_trade_candidate_count,
+ failed_trade_candidate_count: summary.failed_trade_candidate_count,
+ trade_event_count: summary.trade_event_count,
+ pair_candle_count: summary.pair_candle_count,
+ }
+}
+
fn demo_pipeline2_map_local_decoded_event_diagnostic_summary(
summary: kb_lib::LocalDecodedEventDiagnosticSummaryDto,
) -> DemoPipeline2LocalDecodedEventDiagnosticSummary {
diff --git a/kb_demo_app/src/demo_ws.rs b/kb_demo_app/src/demo_ws.rs
index 0c52e81..94c750e 100644
--- a/kb_demo_app/src/demo_ws.rs
+++ b/kb_demo_app/src/demo_ws.rs
@@ -436,8 +436,7 @@ async fn execute_demo_ws_subscribe(
let result = client.account_subscribe_typed(target, config).await;
return result.map_err(|error| format!("account typed subscribe failed: {error}"));
}
- let config_result =
- parse_optional_json_value(&request.config_json, "account raw config");
+ let config_result = parse_optional_json_value(&request.config_json, "account raw config");
let config = match config_result {
Ok(config) => config,
Err(error) => return Err(error),
@@ -526,8 +525,7 @@ async fn execute_demo_ws_subscribe(
let result = client.program_subscribe_typed(target, config).await;
return result.map_err(|error| format!("program typed subscribe failed: {error}"));
}
- let config_result =
- parse_optional_json_value(&request.config_json, "program raw config");
+ let config_result = parse_optional_json_value(&request.config_json, "program raw config");
let config = match config_result {
Ok(config) => config,
Err(error) => return Err(error),
@@ -556,8 +554,7 @@ async fn execute_demo_ws_subscribe(
let result = client.signature_subscribe_typed(target, config).await;
return result.map_err(|error| format!("signature typed subscribe failed: {error}"));
}
- let config_result =
- parse_optional_json_value(&request.config_json, "signature raw config");
+ let config_result = parse_optional_json_value(&request.config_json, "signature raw config");
let config = match config_result {
Ok(config) => config,
Err(error) => return Err(error),
diff --git a/kb_demo_app/src/demo_ws_manager.rs b/kb_demo_app/src/demo_ws_manager.rs
index d0dcab1..999aa46 100644
--- a/kb_demo_app/src/demo_ws_manager.rs
+++ b/kb_demo_app/src/demo_ws_manager.rs
@@ -174,8 +174,7 @@ pub(crate) async fn demo_ws_manager_start_role(
Ok(changed_count) => changed_count,
Err(error) => return Err(error.to_string()),
};
- let action_result =
- build_action_result("start", role.as_str(), matched_count, changed_count);
+ let action_result = build_action_result("start", role.as_str(), matched_count, changed_count);
emit_demo_ws_manager_log(&app_handle, format_action_result_for_log(&action_result).as_str());
emit_demo_ws_manager_snapshot(&app_handle, &state).await;
build_demo_ws_manager_snapshot(&state).await
diff --git a/kb_demo_app/src/main.rs b/kb_demo_app/src/main.rs
index f6ce5f5..df33637 100644
--- a/kb_demo_app/src/main.rs
+++ b/kb_demo_app/src/main.rs
@@ -22,7 +22,7 @@ async fn main() -> std::process::ExitCode {
Err(_err) => {
eprintln!("Cannot create lock!");
std::process::exit(1);
- }
+ },
};
// trying to aquire an exclusive lock
if lock_file.try_lock_exclusive().is_err() {
@@ -32,11 +32,11 @@ async fn main() -> std::process::ExitCode {
if rustls::crypto::CryptoProvider::get_default().is_none() {
let provider_result = rustls::crypto::aws_lc_rs::default_provider().install_default();
match provider_result {
- Ok(()) => {}
+ Ok(()) => {},
Err(error) => {
eprintln!("kb_demo_app rustls provider init error: {:?}", error);
return std::process::ExitCode::FAILURE;
- }
+ },
}
}
let run_result = kb_demo_app_lib::run().await;
diff --git a/kb_demo_app/tauri.conf.json b/kb_demo_app/tauri.conf.json
index 77cee40..70caafc 100644
--- a/kb_demo_app/tauri.conf.json
+++ b/kb_demo_app/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "kb-demo-app",
- "version": "0.7.31",
+ "version": "0.7.32",
"identifier": "com.sasedev.kb-demo-app",
"build": {
"beforeDevCommand": "npm run dev",
diff --git a/kb_lib/src/db.rs b/kb_lib/src/db.rs
index 9214ffa..3325dd0 100644
--- a/kb_lib/src/db.rs
+++ b/kb_lib/src/db.rs
@@ -37,6 +37,7 @@ pub use dtos::LocalMissingTradeEventDiagnosticSampleDto;
pub use dtos::LocalMissingTradeEventReasonSummaryDto;
pub use dtos::LocalMultiTradeSignaturePairDiagnosticSampleDto;
pub use dtos::LocalNonActionablePairDiagnosticSummaryDto;
+pub use dtos::LocalPairActionabilityDiagnosticSummaryDto;
pub use dtos::LocalPairDiagnosticSummaryDto;
pub use dtos::LocalPairGapDiagnosticSampleDto;
pub use dtos::LocalPipelineDiagnosticCountersDto;
@@ -150,6 +151,7 @@ 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;
pub use queries::query_local_non_actionable_pair_diagnostic_list_summaries;
+pub use queries::query_local_pair_actionability_diagnostic_list_summaries;
pub use queries::query_local_pair_diagnostic_list_summaries;
pub use queries::query_local_pair_without_candle_diagnostic_list_samples;
pub use queries::query_local_pair_without_trade_diagnostic_list_samples;
diff --git a/kb_lib/src/db/dtos.rs b/kb_lib/src/db/dtos.rs
index d9b0c3c..32e826e 100644
--- a/kb_lib/src/db/dtos.rs
+++ b/kb_lib/src/db/dtos.rs
@@ -28,6 +28,7 @@ mod pool_listing;
mod pool_origin;
mod pool_token;
mod program_instruction_diagnostic;
+mod program_instruction_discriminator_summary;
mod protocol_candidate;
mod protocol_candidate_summary;
mod swap;
@@ -38,22 +39,21 @@ mod trade_event;
mod transaction_classification;
mod wallet;
mod wallet_holding;
-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::LocalEventClassificationDiagnosticSummaryRow;
pub(crate) use local_pipeline_diagnostics::LocalMissingTradeEventDiagnosticSampleRow;
pub(crate) use local_pipeline_diagnostics::LocalMissingTradeEventReasonSummaryRow;
pub(crate) use local_pipeline_diagnostics::LocalMultiTradeSignaturePairDiagnosticSampleRow;
pub(crate) use local_pipeline_diagnostics::LocalNonActionablePairDiagnosticSummaryRow;
+pub(crate) use local_pipeline_diagnostics::LocalPairActionabilityDiagnosticSummaryRow;
pub(crate) use local_pipeline_diagnostics::LocalPairDiagnosticSummaryRow;
pub(crate) use local_pipeline_diagnostics::LocalPairGapDiagnosticSampleRow;
pub(crate) use local_pipeline_diagnostics::LocalPipelineDiagnosticCountersRow;
-pub use program_instruction_discriminator_summary::ProgramInstructionDiscriminatorSummaryDto;
pub use analysis_signal::AnalysisSignalDto;
pub use chain_instruction::ChainInstructionDto;
pub use chain_slot::ChainSlotDto;
@@ -69,13 +69,14 @@ 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::LocalEventClassificationDiagnosticSummaryDto;
pub use local_pipeline_diagnostics::LocalMissingTradeEventDiagnosticSampleDto;
pub use local_pipeline_diagnostics::LocalMissingTradeEventReasonSummaryDto;
pub use local_pipeline_diagnostics::LocalMultiTradeSignaturePairDiagnosticSampleDto;
pub use local_pipeline_diagnostics::LocalNonActionablePairDiagnosticSummaryDto;
+pub use local_pipeline_diagnostics::LocalPairActionabilityDiagnosticSummaryDto;
pub use local_pipeline_diagnostics::LocalPairDiagnosticSummaryDto;
pub use local_pipeline_diagnostics::LocalPairGapDiagnosticSampleDto;
pub use local_pipeline_diagnostics::LocalPipelineDiagnosticCountersDto;
@@ -91,6 +92,7 @@ pub use pool_listing::PoolListingDto;
pub use pool_origin::PoolOriginDto;
pub use pool_token::PoolTokenDto;
pub use program_instruction_diagnostic::ProgramInstructionDiagnosticDto;
+pub use program_instruction_discriminator_summary::ProgramInstructionDiscriminatorSummaryDto;
pub use protocol_candidate::ProtocolCandidateDto;
pub use protocol_candidate_summary::ProtocolCandidateSummaryDto;
pub use swap::SwapDto;
diff --git a/kb_lib/src/db/dtos/local_pipeline_diagnostics.rs b/kb_lib/src/db/dtos/local_pipeline_diagnostics.rs
index 7525c7f..75ae2b9 100644
--- a/kb_lib/src/db/dtos/local_pipeline_diagnostics.rs
+++ b/kb_lib/src/db/dtos/local_pipeline_diagnostics.rs
@@ -65,14 +65,46 @@ pub struct LocalPipelineDiagnosticSummaryDto {
pub pool_count: i64,
/// Total known pairs.
pub pair_count: i64,
+ /// Stable explanation for legacy pair gap counters.
+ ///
+ /// `pair_without_trade_count` and `pair_without_candle_count` are blocking
+ /// actionable gap counters, not literal catalog-wide counters.
+ pub pair_gap_counter_semantics: std::string::String,
+ /// Total pairs without any persisted trade event, including catalog-only and partial DEX pairs.
+ pub literal_pair_without_trade_count: i64,
+ /// Total pairs without any persisted candle, including catalog-only and partial DEX pairs.
+ pub literal_pair_without_candle_count: i64,
+ /// Total pairs that have at least one persisted trade event.
+ pub trade_materialized_pair_count: i64,
+ /// Total pairs that have at least one persisted candle bucket.
+ pub candle_materialized_pair_count: i64,
+ /// Total pairs with at least one successful decoded trade candidate.
+ pub actionable_pair_count: i64,
+ /// Total distinct candle timeframes currently materialized.
+ pub candle_bucket_timeframe_count: i64,
+ /// Whether candle rows are bucketed aggregates rather than one row per trade.
+ pub candles_are_bucketed: bool,
+ /// Total pairs without trade event among actionable successful trade candidates.
+ pub blocking_pair_without_trade_count: i64,
+ /// Total pairs without candle among actionable successful candle candidates.
+ pub blocking_pair_without_candle_count: i64,
/// Total pairs without trade event.
+ ///
+ /// Legacy alias kept for compatibility. It has the same semantics as
+ /// `blocking_pair_without_trade_count`.
pub pair_without_trade_count: i64,
/// Total pairs without candle.
+ ///
+ /// Legacy alias kept for compatibility. It has the same semantics as
+ /// `blocking_pair_without_candle_count`.
pub pair_without_candle_count: i64,
/// Diagnostics grouped by DEX.
pub dex_summaries: std::vec::Vec,
/// Diagnostics grouped by pair.
pub pair_summaries: std::vec::Vec,
+ /// Diagnostics grouped by pair materialization/actionability class.
+ pub pair_actionability_summaries:
+ std::vec::Vec,
/// Diagnostics grouped by decoded event kind.
pub decoded_event_summaries: std::vec::Vec,
/// Diagnostics grouped by decoded event category, lifecycle kind and actionability.
@@ -157,6 +189,27 @@ pub struct LocalPairDiagnosticSummaryDto {
pub last_price_quote_per_base: std::option::Option,
}
+/// Local pair diagnostics grouped by materialization/actionability class.
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct LocalPairActionabilityDiagnosticSummaryDto {
+ /// Pair actionability or materialization class.
+ pub pair_actionability: std::string::String,
+ /// Total pairs in this class.
+ pub pair_count: i64,
+ /// Total decoded events attached to pairs in this class.
+ pub decoded_event_count: i64,
+ /// Total decoded trade candidates attached to pairs in this class.
+ pub decoded_trade_candidate_count: i64,
+ /// Total decoded trade candidates on successful transactions attached to pairs in this class.
+ pub actionable_trade_candidate_count: i64,
+ /// Total decoded trade candidates on failed transactions attached to pairs in this class.
+ pub failed_trade_candidate_count: i64,
+ /// Total persisted trade events attached to pairs in this class.
+ pub trade_event_count: i64,
+ /// Total persisted candle buckets attached to pairs in this class.
+ pub pair_candle_count: i64,
+}
+
/// Local decoded-event diagnostics summary.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LocalDecodedEventDiagnosticSummaryDto {
@@ -314,8 +367,24 @@ pub struct LocalPipelineDiagnosticCountersDto {
pub pool_count: i64,
/// Total known pairs.
pub pair_count: i64,
+ /// Total pairs without any persisted trade event, including catalog-only and partial DEX pairs.
+ pub literal_pair_without_trade_count: i64,
+ /// Total pairs without any persisted candle, including catalog-only and partial DEX pairs.
+ pub literal_pair_without_candle_count: i64,
+ /// Total pairs that have at least one persisted trade event.
+ pub trade_materialized_pair_count: i64,
+ /// Total pairs that have at least one persisted candle bucket.
+ pub candle_materialized_pair_count: i64,
+ /// Total pairs with at least one successful decoded trade candidate.
+ pub actionable_pair_count: i64,
+ /// Total distinct candle timeframes currently materialized.
+ pub candle_bucket_timeframe_count: i64,
/// Total pairs with only non-actionable missing trade events.
pub non_actionable_pair_count: i64,
+ /// Total pairs without trade among actionable successful trade candidates.
+ pub blocking_pair_without_trade_count: i64,
+ /// Total pairs without candle among actionable successful candle candidates.
+ pub blocking_pair_without_candle_count: i64,
/// Total pairs without trade.
pub pair_without_trade_count: i64,
/// Total pairs without candle.
@@ -351,7 +420,15 @@ pub(crate) struct LocalPipelineDiagnosticCountersRow {
pub(crate) token_metadata_missing_count: i64,
pub(crate) pool_count: i64,
pub(crate) pair_count: i64,
+ pub(crate) literal_pair_without_trade_count: i64,
+ pub(crate) literal_pair_without_candle_count: i64,
+ pub(crate) trade_materialized_pair_count: i64,
+ pub(crate) candle_materialized_pair_count: i64,
+ pub(crate) actionable_pair_count: i64,
+ pub(crate) candle_bucket_timeframe_count: i64,
pub(crate) non_actionable_pair_count: i64,
+ pub(crate) blocking_pair_without_trade_count: i64,
+ pub(crate) blocking_pair_without_candle_count: i64,
pub(crate) pair_without_trade_count: i64,
pub(crate) pair_without_candle_count: i64,
}
@@ -389,6 +466,19 @@ pub(crate) struct LocalPairDiagnosticSummaryRow {
pub(crate) last_price_quote_per_base: std::option::Option,
}
+/// SQL row for local pair actionability diagnostics.
+#[derive(Debug, Clone, sqlx::FromRow)]
+pub(crate) struct LocalPairActionabilityDiagnosticSummaryRow {
+ pub(crate) pair_actionability: std::string::String,
+ pub(crate) pair_count: i64,
+ pub(crate) decoded_event_count: i64,
+ pub(crate) decoded_trade_candidate_count: i64,
+ pub(crate) actionable_trade_candidate_count: i64,
+ pub(crate) failed_trade_candidate_count: i64,
+ pub(crate) trade_event_count: i64,
+ pub(crate) pair_candle_count: i64,
+}
+
/// SQL row for local decoded-event diagnostics.
#[derive(Debug, Clone, sqlx::FromRow)]
pub(crate) struct LocalDecodedEventDiagnosticSummaryRow {
diff --git a/kb_lib/src/db/entities/program_instruction_discriminator_row.rs b/kb_lib/src/db/entities/program_instruction_discriminator_row.rs
index 7142bff..e02775a 100644
--- a/kb_lib/src/db/entities/program_instruction_discriminator_row.rs
+++ b/kb_lib/src/db/entities/program_instruction_discriminator_row.rs
@@ -1,4 +1,3 @@
-
// file: kb_lib/src/db/entities/program_instruction_discriminator_row.rs
//! Program instruction discriminator diagnostic row entity.
diff --git a/kb_lib/src/db/queries.rs b/kb_lib/src/db/queries.rs
index 8f3ac91..1e36b85 100644
--- a/kb_lib/src/db/queries.rs
+++ b/kb_lib/src/db/queries.rs
@@ -88,6 +88,7 @@ pub use local_pipeline_diagnostics::query_local_missing_trade_event_diagnostic_l
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;
pub use local_pipeline_diagnostics::query_local_non_actionable_pair_diagnostic_list_summaries;
+pub use local_pipeline_diagnostics::query_local_pair_actionability_diagnostic_list_summaries;
pub use local_pipeline_diagnostics::query_local_pair_diagnostic_list_summaries;
pub use local_pipeline_diagnostics::query_local_pair_without_candle_diagnostic_list_samples;
pub use local_pipeline_diagnostics::query_local_pair_without_trade_diagnostic_list_samples;
diff --git a/kb_lib/src/db/queries/local_pipeline_diagnostics.rs b/kb_lib/src/db/queries/local_pipeline_diagnostics.rs
index f9055bb..f531d68 100644
--- a/kb_lib/src/db/queries/local_pipeline_diagnostics.rs
+++ b/kb_lib/src/db/queries/local_pipeline_diagnostics.rs
@@ -188,6 +188,51 @@ SELECT
) AS token_metadata_missing_count,
(SELECT COUNT(*) FROM k_sol_pools) AS pool_count,
(SELECT COUNT(*) FROM k_sol_pairs) AS pair_count,
+ (
+ SELECT COUNT(*)
+ FROM (
+ SELECT pair.id
+ FROM k_sol_pairs pair
+ LEFT JOIN k_sol_trade_events te ON te.pair_id = pair.id
+ GROUP BY pair.id
+ HAVING COUNT(DISTINCT te.id) = 0
+ )
+ ) AS literal_pair_without_trade_count,
+ (
+ SELECT COUNT(*)
+ FROM (
+ SELECT pair.id
+ FROM k_sol_pairs pair
+ LEFT JOIN k_sol_pair_candles pc ON pc.pair_id = pair.id
+ GROUP BY pair.id
+ HAVING COUNT(DISTINCT pc.bucket_start_unix || ':' || pc.timeframe_seconds) = 0
+ )
+ ) AS literal_pair_without_candle_count,
+ (
+ SELECT COUNT(DISTINCT pair_id)
+ FROM k_sol_trade_events
+ ) AS trade_materialized_pair_count,
+ (
+ SELECT COUNT(DISTINCT pair_id)
+ FROM k_sol_pair_candles
+ ) AS candle_materialized_pair_count,
+ (
+ SELECT COUNT(*)
+ FROM (
+ SELECT pair.id
+ FROM k_sol_pairs pair
+ JOIN k_sol_pools p ON p.id = pair.pool_id
+ JOIN k_sol_dex_decoded_events dde ON dde.pool_account = p.address
+ JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id
+ WHERE json_extract(dde.payload_json, '$.tradeCandidate') = 1
+ AND ct.err_json IS NULL
+ GROUP BY pair.id
+ )
+ ) AS actionable_pair_count,
+ (
+ SELECT COUNT(DISTINCT timeframe_seconds)
+ FROM k_sol_pair_candles
+ ) AS candle_bucket_timeframe_count,
(
SELECT COUNT(*)
FROM (
@@ -216,6 +261,24 @@ SELECT
AND COUNT(DISTINCT dde.id) > 0
)
) AS non_actionable_pair_count,
+ (
+ SELECT COUNT(*)
+ FROM (
+ SELECT pair.id
+ FROM k_sol_pairs pair
+ JOIN k_sol_pools p ON p.id = pair.pool_id
+ LEFT JOIN k_sol_dex_decoded_events dde ON dde.pool_account = p.address
+ LEFT JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id
+ LEFT JOIN k_sol_trade_events te ON te.pair_id = pair.id
+ GROUP BY pair.id
+ HAVING COUNT(DISTINCT CASE
+ WHEN json_extract(dde.payload_json, '$.tradeCandidate') = 1
+ AND ct.id IS NOT NULL
+ AND ct.err_json IS NULL
+ THEN dde.id END) > 0
+ AND COUNT(DISTINCT te.id) = 0
+ )
+ ) AS blocking_pair_without_trade_count,
(
SELECT COUNT(*)
FROM (
@@ -234,6 +297,24 @@ SELECT
AND COUNT(DISTINCT te.id) = 0
)
) AS pair_without_trade_count,
+ (
+ SELECT COUNT(*)
+ FROM (
+ SELECT pair.id
+ FROM k_sol_pairs pair
+ JOIN k_sol_pools p ON p.id = pair.pool_id
+ LEFT JOIN k_sol_dex_decoded_events dde ON dde.pool_account = p.address
+ LEFT JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id
+ LEFT JOIN k_sol_pair_candles pc ON pc.pair_id = pair.id
+ GROUP BY pair.id
+ HAVING COUNT(DISTINCT CASE
+ WHEN json_extract(dde.payload_json, '$.candleCandidate') = 1
+ AND ct.id IS NOT NULL
+ AND ct.err_json IS NULL
+ THEN dde.id END) > 0
+ AND COUNT(DISTINCT pc.bucket_start_unix || ':' || pc.timeframe_seconds) = 0
+ )
+ ) AS blocking_pair_without_candle_count,
(
SELECT COUNT(*)
FROM (
@@ -295,7 +376,15 @@ SELECT
token_metadata_missing_count: row.token_metadata_missing_count,
pool_count: row.pool_count,
pair_count: row.pair_count,
+ literal_pair_without_trade_count: row.literal_pair_without_trade_count,
+ literal_pair_without_candle_count: row.literal_pair_without_candle_count,
+ trade_materialized_pair_count: row.trade_materialized_pair_count,
+ candle_materialized_pair_count: row.candle_materialized_pair_count,
+ actionable_pair_count: row.actionable_pair_count,
+ candle_bucket_timeframe_count: row.candle_bucket_timeframe_count,
non_actionable_pair_count: row.non_actionable_pair_count,
+ blocking_pair_without_trade_count: row.blocking_pair_without_trade_count,
+ blocking_pair_without_candle_count: row.blocking_pair_without_candle_count,
pair_without_trade_count: row.pair_without_trade_count,
pair_without_candle_count: row.pair_without_candle_count,
actionable_missing_trade_event_count: row.actionable_missing_trade_event_count,
@@ -523,6 +612,112 @@ ORDER BY pair.id
}
}
+/// Lists local pair materialization/actionability summaries.
+pub async fn query_local_pair_actionability_diagnostic_list_summaries(
+ database: &crate::Database,
+) -> Result, crate::Error> {
+ match database.connection() {
+ crate::DatabaseConnection::Sqlite(pool) => {
+ let rows_result = sqlx::query_as::<
+ sqlx::Sqlite,
+ crate::db::dtos::LocalPairActionabilityDiagnosticSummaryRow,
+ >(
+ r#"
+WITH pair_state AS (
+ SELECT
+ pair.id AS pair_id,
+ COUNT(DISTINCT dde.id) AS decoded_event_count,
+ COUNT(DISTINCT CASE WHEN json_extract(dde.payload_json, '$.tradeCandidate') = 1 THEN dde.id END) AS decoded_trade_candidate_count,
+ COUNT(DISTINCT CASE
+ WHEN json_extract(dde.payload_json, '$.tradeCandidate') = 1
+ AND ct.id IS NOT NULL
+ AND ct.err_json IS NULL
+ THEN dde.id END) AS actionable_trade_candidate_count,
+ COUNT(DISTINCT CASE
+ WHEN json_extract(dde.payload_json, '$.tradeCandidate') = 1
+ AND ct.id IS NOT NULL
+ AND ct.err_json IS NOT NULL
+ THEN dde.id END) AS failed_trade_candidate_count,
+ COUNT(DISTINCT te.id) AS trade_event_count,
+ COUNT(DISTINCT pc.bucket_start_unix || ':' || pc.timeframe_seconds) AS pair_candle_count
+ FROM k_sol_pairs pair
+ JOIN k_sol_pools p ON p.id = pair.pool_id
+ LEFT JOIN k_sol_dex_decoded_events dde ON dde.pool_account = p.address
+ LEFT JOIN k_sol_chain_transactions ct ON ct.id = dde.transaction_id
+ LEFT JOIN k_sol_trade_events te ON te.pair_id = pair.id
+ LEFT JOIN k_sol_pair_candles pc ON pc.pair_id = pair.id
+ GROUP BY pair.id
+), classified AS (
+ SELECT
+ CASE
+ WHEN trade_event_count > 0 THEN 'trade_materialized'
+ WHEN actionable_trade_candidate_count > 0 THEN 'actionable_without_materialized_trade'
+ WHEN failed_trade_candidate_count > 0 THEN 'failed_trade_candidate_only'
+ WHEN decoded_trade_candidate_count > 0 THEN 'non_actionable_trade_candidate_only'
+ WHEN decoded_event_count > 0 THEN 'decoded_without_trade_candidate'
+ ELSE 'catalog_only'
+ END AS pair_actionability,
+ pair_id,
+ decoded_event_count,
+ decoded_trade_candidate_count,
+ actionable_trade_candidate_count,
+ failed_trade_candidate_count,
+ trade_event_count,
+ pair_candle_count
+ FROM pair_state
+)
+SELECT
+ pair_actionability AS pair_actionability,
+ COUNT(pair_id) AS pair_count,
+ SUM(decoded_event_count) AS decoded_event_count,
+ SUM(decoded_trade_candidate_count) AS decoded_trade_candidate_count,
+ SUM(actionable_trade_candidate_count) AS actionable_trade_candidate_count,
+ SUM(failed_trade_candidate_count) AS failed_trade_candidate_count,
+ SUM(trade_event_count) AS trade_event_count,
+ SUM(pair_candle_count) AS pair_candle_count
+FROM classified
+GROUP BY pair_actionability
+ORDER BY
+ CASE pair_actionability
+ WHEN 'trade_materialized' THEN 1
+ WHEN 'actionable_without_materialized_trade' THEN 2
+ WHEN 'failed_trade_candidate_only' THEN 3
+ WHEN 'non_actionable_trade_candidate_only' THEN 4
+ WHEN 'decoded_without_trade_candidate' THEN 5
+ ELSE 6
+ END,
+ pair_actionability
+ "#,
+ )
+ .fetch_all(pool)
+ .await;
+ let rows = match rows_result {
+ Ok(rows) => rows,
+ Err(error) => {
+ return Err(crate::Error::Db(format!(
+ "cannot list local pair actionability diagnostic summaries on sqlite: {}",
+ error
+ )));
+ },
+ };
+ let mut summaries = std::vec::Vec::new();
+ for row in rows {
+ summaries.push(crate::LocalPairActionabilityDiagnosticSummaryDto {
+ pair_actionability: row.pair_actionability,
+ pair_count: row.pair_count,
+ decoded_event_count: row.decoded_event_count,
+ decoded_trade_candidate_count: row.decoded_trade_candidate_count,
+ actionable_trade_candidate_count: row.actionable_trade_candidate_count,
+ failed_trade_candidate_count: row.failed_trade_candidate_count,
+ trade_event_count: row.trade_event_count,
+ pair_candle_count: row.pair_candle_count,
+ });
+ }
+ return Ok(summaries);
+ },
+ }
+}
+
/// Lists local decoded-event diagnostic summaries.
pub async fn query_local_decoded_event_diagnostic_list_summaries(
database: &crate::Database,
diff --git a/kb_lib/src/dex_support_matrix.rs b/kb_lib/src/dex_support_matrix.rs
index fbb9ef6..bb31c16 100644
--- a/kb_lib/src/dex_support_matrix.rs
+++ b/kb_lib/src/dex_support_matrix.rs
@@ -848,19 +848,18 @@ mod tests {
#[test]
fn matrix_lookup_by_program_id_returns_expected_protocol() {
- let entry = match crate::dex_support_matrix_entry_by_program_id(crate::METEORA_DLMM_PROGRAM_ID)
- {
- Some(entry) => entry,
- None => panic!("expected meteora_dlmm program id lookup"),
- };
+ let entry =
+ match crate::dex_support_matrix_entry_by_program_id(crate::METEORA_DLMM_PROGRAM_ID) {
+ Some(entry) => entry,
+ None => panic!("expected meteora_dlmm program id lookup"),
+ };
assert_eq!(entry.code, "meteora_dlmm");
- let raydium_entry = match crate::dex_support_matrix_entry_by_program_id(
- crate::RAYDIUM_AMM_V4_PROGRAM_ID,
- ) {
- Some(entry) => entry,
- None => panic!("expected raydium AMM v4 program id lookup"),
- };
+ let raydium_entry =
+ match crate::dex_support_matrix_entry_by_program_id(crate::RAYDIUM_AMM_V4_PROGRAM_ID) {
+ Some(entry) => entry,
+ None => panic!("expected raydium AMM v4 program id lookup"),
+ };
assert_eq!(raydium_entry.code, "raydium_amm_v4");
}
diff --git a/kb_lib/src/lib.rs b/kb_lib/src/lib.rs
index 07bc89d..3e51997 100644
--- a/kb_lib/src/lib.rs
+++ b/kb_lib/src/lib.rs
@@ -357,6 +357,8 @@ pub use db::LocalMissingTradeEventReasonSummaryDto;
pub use db::LocalMultiTradeSignaturePairDiagnosticSampleDto;
/// Local pair diagnostics for pairs whose missing trade events are non-actionable.
pub use db::LocalNonActionablePairDiagnosticSummaryDto;
+/// Local pair diagnostics grouped by materialization/actionability class.
+pub use db::LocalPairActionabilityDiagnosticSummaryDto;
/// Local pair diagnostics summary.
pub use db::LocalPairDiagnosticSummaryDto;
/// Sample of a pair gap.
@@ -571,6 +573,8 @@ pub use db::query_local_missing_trade_event_reason_list_summaries;
pub use db::query_local_multi_trade_signature_pair_diagnostic_list_samples;
/// Lists pair summaries for non-actionable missing trade events.
pub use db::query_local_non_actionable_pair_diagnostic_list_summaries;
+/// Lists local pair materialization/actionability summaries.
+pub use db::query_local_pair_actionability_diagnostic_list_summaries;
/// Lists local pair diagnostic summaries.
pub use db::query_local_pair_diagnostic_list_summaries;
/// Lists samples of pairs without candles.
diff --git a/kb_lib/src/local_pipeline_diagnostics.rs b/kb_lib/src/local_pipeline_diagnostics.rs
index 24033e9..32c29aa 100644
--- a/kb_lib/src/local_pipeline_diagnostics.rs
+++ b/kb_lib/src/local_pipeline_diagnostics.rs
@@ -35,6 +35,13 @@ impl LocalPipelineDiagnosticsService {
Ok(pair_summaries) => pair_summaries,
Err(error) => return Err(error),
};
+ let pair_actionability_summaries_result =
+ crate::query_local_pair_actionability_diagnostic_list_summaries(self.database.as_ref())
+ .await;
+ let pair_actionability_summaries = match pair_actionability_summaries_result {
+ Ok(summaries) => summaries,
+ Err(error) => return Err(error),
+ };
let decoded_event_summaries_result =
crate::query_local_decoded_event_diagnostic_list_summaries(self.database.as_ref())
.await;
@@ -160,10 +167,21 @@ impl LocalPipelineDiagnosticsService {
token_metadata_missing_count: counters.token_metadata_missing_count,
pool_count: counters.pool_count,
pair_count: counters.pair_count,
+ pair_gap_counter_semantics: "blocking_actionable_pairs_only".to_string(),
+ literal_pair_without_trade_count: counters.literal_pair_without_trade_count,
+ literal_pair_without_candle_count: counters.literal_pair_without_candle_count,
+ trade_materialized_pair_count: counters.trade_materialized_pair_count,
+ candle_materialized_pair_count: counters.candle_materialized_pair_count,
+ actionable_pair_count: counters.actionable_pair_count,
+ candle_bucket_timeframe_count: counters.candle_bucket_timeframe_count,
+ candles_are_bucketed: counters.candle_bucket_timeframe_count > 0,
+ blocking_pair_without_trade_count: counters.blocking_pair_without_trade_count,
+ blocking_pair_without_candle_count: counters.blocking_pair_without_candle_count,
pair_without_trade_count: counters.pair_without_trade_count,
pair_without_candle_count: counters.pair_without_candle_count,
dex_summaries,
pair_summaries,
+ pair_actionability_summaries,
decoded_event_summaries,
event_classification_summaries,
missing_trade_event_reason_summaries,
diff --git a/kb_lib/src/local_pipeline_validation.rs b/kb_lib/src/local_pipeline_validation.rs
index cd8701f..2f43e63 100644
--- a/kb_lib/src/local_pipeline_validation.rs
+++ b/kb_lib/src/local_pipeline_validation.rs
@@ -40,6 +40,8 @@ pub struct LocalPipelineValidationConfig {
pub require_candles_per_dex: bool,
/// Whether non-actionable classification groups must have no linked trade events.
pub require_no_non_actionable_trade_events_materialized: bool,
+ /// Whether the DEX support matrix must satisfy internal semantic invariants.
+ pub require_dex_support_matrix_semantics: bool,
}
impl Default for LocalPipelineValidationConfig {
@@ -59,6 +61,7 @@ impl Default for LocalPipelineValidationConfig {
require_trade_events_per_dex: true,
require_candles_per_dex: true,
require_no_non_actionable_trade_events_materialized: true,
+ require_dex_support_matrix_semantics: false,
};
}
}
@@ -86,6 +89,7 @@ impl LocalPipelineValidationConfig {
require_trade_events_per_dex: true,
require_candles_per_dex: true,
require_no_non_actionable_trade_events_materialized: true,
+ require_dex_support_matrix_semantics: false,
};
}
@@ -118,6 +122,7 @@ impl LocalPipelineValidationConfig {
require_trade_events_per_dex: false,
require_candles_per_dex: false,
require_no_non_actionable_trade_events_materialized: true,
+ require_dex_support_matrix_semantics: false,
};
}
@@ -149,6 +154,7 @@ impl LocalPipelineValidationConfig {
require_trade_events_per_dex: false,
require_candles_per_dex: false,
require_no_non_actionable_trade_events_materialized: true,
+ require_dex_support_matrix_semantics: false,
};
}
@@ -174,6 +180,17 @@ impl LocalPipelineValidationConfig {
config.require_no_non_actionable_trade_events_materialized = true;
return config;
}
+
+ /// Builds the `0.7.32` validation report semantics config.
+ ///
+ /// This profile keeps the `0.7.31` trade-actionability policy and validates
+ /// the support matrix semantics used to interpret partial/planned DEXes.
+ pub fn v0_7_32_validation_report_semantics() -> Self {
+ let mut config = Self::v0_7_31_trade_event_actionability_policy();
+ config.profile_code = "0.7.32_validation_report_semantics".to_string();
+ config.require_dex_support_matrix_semantics = true;
+ return config;
+ }
}
/// A single local pipeline validation issue.
@@ -318,6 +335,14 @@ impl LocalPipelineValidationService {
crate::LocalPipelineValidationConfig::v0_7_31_trade_event_actionability_policy();
return self.validate_current_database(&config).await;
}
+
+ /// Diagnoses the current database with the `0.7.32` validation report semantics profile.
+ pub async fn validate_v0_7_32_current_database(
+ &self,
+ ) -> Result {
+ let config = crate::LocalPipelineValidationConfig::v0_7_32_validation_report_semantics();
+ return self.validate_current_database(&config).await;
+ }
}
/// Validates a diagnostics summary without performing database access.
@@ -429,6 +454,9 @@ pub fn validate_local_pipeline_diagnostics_summary(
}
}
}
+ if config.require_dex_support_matrix_semantics {
+ validate_dex_support_matrix_semantics(&mut issues);
+ }
if config.require_all_expected_dexes {
for expected_dex_code in &expected_dex_codes {
if !observed_dex_codes.contains(expected_dex_code) {
@@ -504,6 +532,65 @@ pub fn validate_local_pipeline_diagnostics_summary(
};
}
+fn validate_dex_support_matrix_semantics(
+ issues: &mut std::vec::Vec,
+) {
+ let entries = crate::dex_support_matrix_entry_dtos();
+ for entry in entries {
+ let entry_code = entry.code.clone();
+ if entry.status == "supported" {
+ let supported_materialization_ok = entry.decoded
+ && entry.materialized
+ && entry.trade_candidate
+ && entry.candle_candidate
+ && entry.pair_candidate
+ && entry.pool_candidate
+ && entry.catalog_enabled;
+ if !supported_materialization_ok {
+ issues.push(crate::LocalPipelineValidationIssueDto {
+ code: "supported_dex_matrix_entry_not_fully_materialized".to_string(),
+ message: format!(
+ "supported DEX '{}' must expose decoded/materialized trade, candle, pair and pool capabilities",
+ entry_code
+ ),
+ subject: Some(entry_code.clone()),
+ blocking: true,
+ });
+ }
+ }
+ if entry.status == "partial" {
+ let has_skip_reason = match &entry.skip_reason {
+ Some(skip_reason) => !skip_reason.trim().is_empty(),
+ None => false,
+ };
+ if !has_skip_reason {
+ issues.push(crate::LocalPipelineValidationIssueDto {
+ code: "partial_dex_matrix_entry_without_skip_reason".to_string(),
+ message: format!(
+ "partial DEX '{}' must expose an explicit skip reason",
+ entry_code
+ ),
+ subject: Some(entry_code.clone()),
+ blocking: true,
+ });
+ }
+ }
+ if (entry.status == "planned" || entry.status == "to_verify" || entry.status == "unknown")
+ && entry.catalog_enabled
+ {
+ issues.push(crate::LocalPipelineValidationIssueDto {
+ code: "inactive_dex_matrix_entry_catalog_enabled".to_string(),
+ message: format!(
+ "inactive DEX '{}' must not be enabled in the runtime catalog",
+ entry_code
+ ),
+ subject: Some(entry_code.clone()),
+ blocking: true,
+ });
+ }
+ }
+}
+
fn build_local_pipeline_validation_run(
summary: crate::LocalPipelineDiagnosticSummaryDto,
config: &crate::LocalPipelineValidationConfig,
@@ -563,6 +650,16 @@ mod tests {
token_metadata_missing_count: 0,
pool_count: 27,
pair_count: 27,
+ pair_gap_counter_semantics: "blocking_actionable_pairs_only".to_string(),
+ literal_pair_without_trade_count: 4,
+ literal_pair_without_candle_count: 3,
+ trade_materialized_pair_count: 23,
+ candle_materialized_pair_count: 24,
+ actionable_pair_count: 23,
+ candle_bucket_timeframe_count: 4,
+ candles_are_bucketed: true,
+ blocking_pair_without_trade_count: 0,
+ blocking_pair_without_candle_count: 0,
non_actionable_pair_count: 0,
pair_without_trade_count: 0,
pair_without_candle_count: 0,
@@ -609,6 +706,28 @@ mod tests {
},
],
pair_summaries: vec![],
+ pair_actionability_summaries: vec![
+ crate::LocalPairActionabilityDiagnosticSummaryDto {
+ pair_actionability: "trade_materialized".to_string(),
+ pair_count: 23,
+ decoded_event_count: 210,
+ decoded_trade_candidate_count: 210,
+ actionable_trade_candidate_count: 210,
+ failed_trade_candidate_count: 0,
+ trade_event_count: 210,
+ pair_candle_count: 230,
+ },
+ crate::LocalPairActionabilityDiagnosticSummaryDto {
+ pair_actionability: "catalog_only".to_string(),
+ pair_count: 4,
+ decoded_event_count: 0,
+ decoded_trade_candidate_count: 0,
+ actionable_trade_candidate_count: 0,
+ failed_trade_candidate_count: 0,
+ trade_event_count: 0,
+ pair_candle_count: 0,
+ },
+ ],
decoded_event_summaries: vec![],
event_classification_summaries: vec![],
missing_trade_event_reason_summaries: vec![],
@@ -746,6 +865,24 @@ mod tests {
assert_eq!(report.validation_profile_code, "0.7.31_trade_event_actionability_policy");
}
+ #[test]
+ fn validation_accepts_0_7_32_validation_report_semantics_summary() {
+ let mut summary = make_0_7_28_summary_with_meteora();
+ summary.literal_pair_without_trade_count = 12;
+ summary.literal_pair_without_candle_count = 12;
+ summary.blocking_pair_without_trade_count = 0;
+ summary.blocking_pair_without_candle_count = 0;
+ summary.pair_without_trade_count = 0;
+ summary.pair_without_candle_count = 0;
+ summary.pair_gap_counter_semantics = "blocking_actionable_pairs_only".to_string();
+ let config = crate::LocalPipelineValidationConfig::v0_7_32_validation_report_semantics();
+ let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
+ assert!(report.validation_passed);
+ assert_eq!(report.validation_profile_code, "0.7.32_validation_report_semantics");
+ assert_eq!(summary.literal_pair_without_trade_count, 12);
+ assert_eq!(summary.blocking_pair_without_trade_count, 0);
+ }
+
#[test]
fn validation_rejects_0_7_31_failed_transaction_trade_events() {
let mut summary = make_0_7_28_summary_with_meteora();
@@ -790,6 +927,42 @@ mod tests {
assert!(found_pump_swap);
}
+ #[test]
+ fn partial_dex_matrix_entries_have_skip_reasons() {
+ for entry in crate::dex_support_matrix_entry_dtos() {
+ if entry.status == "partial" {
+ let has_skip_reason = match entry.skip_reason {
+ Some(skip_reason) => !skip_reason.trim().is_empty(),
+ None => false,
+ };
+ assert!(has_skip_reason, "{}", entry.code);
+ }
+ }
+ }
+
+ #[test]
+ fn supported_dex_matrix_entries_have_materialization_flags() {
+ for entry in crate::dex_support_matrix_entry_dtos() {
+ if entry.status == "supported" {
+ assert!(entry.decoded, "{}", entry.code);
+ assert!(entry.materialized, "{}", entry.code);
+ assert!(entry.trade_candidate, "{}", entry.code);
+ assert!(entry.candle_candidate, "{}", entry.code);
+ assert!(entry.pair_candidate, "{}", entry.code);
+ assert!(entry.pool_candidate, "{}", entry.code);
+ }
+ }
+ }
+
+ #[test]
+ fn planned_dex_matrix_entries_are_not_catalog_enabled() {
+ for entry in crate::dex_support_matrix_entry_dtos() {
+ if entry.status == "planned" || entry.status == "to_verify" {
+ assert!(!entry.catalog_enabled, "{}", entry.code);
+ }
+ }
+ }
+
#[test]
fn validation_rejects_missing_expected_dex() {
let mut summary = make_clean_summary();