This commit is contained in:
2026-05-13 13:19:23 +02:00
parent 24d21818cf
commit 693a456e62
19 changed files with 631 additions and 40 deletions

View File

@@ -40,6 +40,7 @@ pub use dtos::LocalNonActionablePairDiagnosticSummaryDto;
pub use dtos::LocalPairActionabilityDiagnosticSummaryDto;
pub use dtos::LocalPairDiagnosticSummaryDto;
pub use dtos::LocalPairGapDiagnosticSampleDto;
pub use dtos::LocalPairTradingReadinessDiagnosticSummaryDto;
pub use dtos::LocalPipelineDiagnosticCountersDto;
pub use dtos::LocalPipelineDiagnosticSummaryDto;
pub use dtos::ObservedTokenDto;
@@ -153,6 +154,7 @@ 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_trading_readiness_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;
pub use queries::query_local_pipeline_diagnostic_get_counters;

View File

@@ -52,6 +52,7 @@ pub(crate) use local_pipeline_diagnostics::LocalNonActionablePairDiagnosticSumma
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::LocalPairTradingReadinessDiagnosticSummaryRow;
pub(crate) use local_pipeline_diagnostics::LocalPipelineDiagnosticCountersRow;
pub use analysis_signal::AnalysisSignalDto;
@@ -79,6 +80,7 @@ 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::LocalPairTradingReadinessDiagnosticSummaryDto;
pub use local_pipeline_diagnostics::LocalPipelineDiagnosticCountersDto;
pub use local_pipeline_diagnostics::LocalPipelineDiagnosticSummaryDto;
pub use observed_token::ObservedTokenDto;

View File

@@ -105,6 +105,9 @@ pub struct LocalPipelineDiagnosticSummaryDto {
/// Diagnostics grouped by pair materialization/actionability class.
pub pair_actionability_summaries:
std::vec::Vec<crate::LocalPairActionabilityDiagnosticSummaryDto>,
/// Diagnostics grouped by pair trading readiness class.
pub pair_trading_readiness_summaries:
std::vec::Vec<crate::LocalPairTradingReadinessDiagnosticSummaryDto>,
/// 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.
@@ -187,6 +190,12 @@ pub struct LocalPairDiagnosticSummaryDto {
pub pair_candle_count: i64,
/// Last known price.
pub last_price_quote_per_base: std::option::Option<f64>,
/// Pair trading-readiness class derived from base/quote orientation.
pub pair_trading_readiness: std::string::String,
/// Quote asset class used by the readiness classifier.
pub quote_asset_class: std::string::String,
/// Whether the pair likely requires a router or aggregator before direct bot execution.
pub trading_route_required: bool,
}
/// Local pair diagnostics grouped by materialization/actionability class.
@@ -210,6 +219,27 @@ pub struct LocalPairActionabilityDiagnosticSummaryDto {
pub pair_candle_count: i64,
}
/// Local pair diagnostics grouped by trading readiness class.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LocalPairTradingReadinessDiagnosticSummaryDto {
/// Pair trading-readiness class.
pub pair_trading_readiness: std::string::String,
/// Quote asset class attached to this readiness group.
pub quote_asset_class: std::string::String,
/// Whether the group requires a router or aggregator before direct execution.
pub trading_route_required: bool,
/// Total pairs in this readiness group.
pub pair_count: i64,
/// Total decoded events attached to pairs in this readiness group.
pub decoded_event_count: i64,
/// Total decoded trade candidates attached to pairs in this readiness group.
pub decoded_trade_candidate_count: i64,
/// Total persisted trade events attached to pairs in this readiness group.
pub trade_event_count: i64,
/// Total persisted candle buckets attached to pairs in this readiness group.
pub pair_candle_count: i64,
}
/// Local decoded-event diagnostics summary.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LocalDecodedEventDiagnosticSummaryDto {
@@ -464,6 +494,9 @@ pub(crate) struct LocalPairDiagnosticSummaryRow {
pub(crate) invalid_trade_event_count: i64,
pub(crate) pair_candle_count: i64,
pub(crate) last_price_quote_per_base: std::option::Option<f64>,
pub(crate) pair_trading_readiness: std::string::String,
pub(crate) quote_asset_class: std::string::String,
pub(crate) trading_route_required: i64,
}
/// SQL row for local pair actionability diagnostics.
@@ -479,6 +512,19 @@ pub(crate) struct LocalPairActionabilityDiagnosticSummaryRow {
pub(crate) pair_candle_count: i64,
}
/// SQL row for local pair trading-readiness diagnostics.
#[derive(Debug, Clone, sqlx::FromRow)]
pub(crate) struct LocalPairTradingReadinessDiagnosticSummaryRow {
pub(crate) pair_trading_readiness: std::string::String,
pub(crate) quote_asset_class: std::string::String,
pub(crate) trading_route_required: i64,
pub(crate) pair_count: i64,
pub(crate) decoded_event_count: i64,
pub(crate) decoded_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 {

View File

@@ -90,6 +90,7 @@ pub use local_pipeline_diagnostics::query_local_multi_trade_signature_pair_diagn
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_trading_readiness_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;
pub use local_pipeline_diagnostics::query_local_pipeline_diagnostic_get_counters;

View File

@@ -555,7 +555,56 @@ SELECT
WHERE te_last.pair_id = pair.id
ORDER BY te_last.id DESC
LIMIT 1
) AS last_price_quote_per_base
) AS last_price_quote_per_base,
CASE
WHEN COUNT(DISTINCT te.id) = 0 THEN 'non_trade_materialized'
WHEN quote_token.mint = 'So11111111111111111111111111111111111111112' THEN 'direct_wsol_quote'
WHEN quote_token.mint IN (
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB',
'JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD'
) THEN 'direct_stable_quote'
WHEN base_token.mint = 'So11111111111111111111111111111111111111112' THEN 'inverse_wsol_base'
WHEN base_token.mint IN (
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB',
'JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD'
) THEN 'inverse_stable_base'
WHEN quote_token.mint IS NULL OR quote_token.mint = '' THEN 'unknown_quote'
ELSE 'cross_quote_requires_router'
END AS pair_trading_readiness,
CASE
WHEN quote_token.mint = 'So11111111111111111111111111111111111111112' THEN 'wsol'
WHEN quote_token.mint IN (
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB',
'JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD'
) THEN 'stable'
WHEN quote_token.mint IS NULL OR quote_token.mint = '' THEN 'unknown'
ELSE 'other'
END AS quote_asset_class,
CASE
WHEN COUNT(DISTINCT te.id) = 0 THEN 0
WHEN quote_token.mint IS NULL OR quote_token.mint = '' THEN 1
WHEN quote_token.mint = 'So11111111111111111111111111111111111111112' THEN 0
WHEN quote_token.mint IN (
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB',
'JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD'
) THEN 0
WHEN base_token.mint = 'So11111111111111111111111111111111111111112' THEN 0
WHEN base_token.mint IN (
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB',
'JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD'
) THEN 0
ELSE 1
END AS trading_route_required
FROM k_sol_pairs pair
JOIN k_sol_pools p ON p.id = pair.pool_id
JOIN k_sol_dexes d ON d.id = p.dex_id
@@ -605,6 +654,9 @@ ORDER BY pair.id
invalid_trade_event_count: row.invalid_trade_event_count,
pair_candle_count: row.pair_candle_count,
last_price_quote_per_base: row.last_price_quote_per_base,
pair_trading_readiness: row.pair_trading_readiness,
quote_asset_class: row.quote_asset_class,
trading_route_required: row.trading_route_required != 0,
});
}
return Ok(summaries);
@@ -716,6 +768,146 @@ ORDER BY
return Ok(summaries);
},
}
}
/// Lists local pair trading-readiness summaries.
pub async fn query_local_pair_trading_readiness_diagnostic_list_summaries(
database: &crate::Database,
) -> Result<std::vec::Vec<crate::LocalPairTradingReadinessDiagnosticSummaryDto>, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let rows_result = sqlx::query_as::<
sqlx::Sqlite,
crate::db::dtos::LocalPairTradingReadinessDiagnosticSummaryRow,
>(
r#"
WITH pair_state AS (
SELECT
pair.id AS pair_id,
base_token.mint AS base_mint,
quote_token.mint AS quote_mint,
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 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
JOIN k_sol_tokens base_token ON base_token.id = pair.base_token_id
JOIN k_sol_tokens quote_token ON quote_token.id = pair.quote_token_id
LEFT JOIN k_sol_dex_decoded_events dde ON dde.pool_account = p.address
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, base_token.mint, quote_token.mint
), classified AS (
SELECT
CASE
WHEN trade_event_count = 0 THEN 'non_trade_materialized'
WHEN quote_mint = 'So11111111111111111111111111111111111111112' THEN 'direct_wsol_quote'
WHEN quote_mint IN (
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB',
'JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD'
) THEN 'direct_stable_quote'
WHEN base_mint = 'So11111111111111111111111111111111111111112' THEN 'inverse_wsol_base'
WHEN base_mint IN (
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB',
'JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD'
) THEN 'inverse_stable_base'
WHEN quote_mint IS NULL OR quote_mint = '' THEN 'unknown_quote'
ELSE 'cross_quote_requires_router'
END AS pair_trading_readiness,
CASE
WHEN quote_mint = 'So11111111111111111111111111111111111111112' THEN 'wsol'
WHEN quote_mint IN (
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB',
'JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD'
) THEN 'stable'
WHEN quote_mint IS NULL OR quote_mint = '' THEN 'unknown'
ELSE 'other'
END AS quote_asset_class,
CASE
WHEN trade_event_count = 0 THEN 0
WHEN quote_mint IS NULL OR quote_mint = '' THEN 1
WHEN quote_mint = 'So11111111111111111111111111111111111111112' THEN 0
WHEN quote_mint IN (
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB',
'JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD'
) THEN 0
WHEN base_mint = 'So11111111111111111111111111111111111111112' THEN 0
WHEN base_mint IN (
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',
'USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB',
'JuprjznTrTSp2UFa3ZBUFgwdAmtZCq4MQCwysN55USD'
) THEN 0
ELSE 1
END AS trading_route_required,
pair_id,
decoded_event_count,
decoded_trade_candidate_count,
trade_event_count,
pair_candle_count
FROM pair_state
)
SELECT
pair_trading_readiness AS pair_trading_readiness,
quote_asset_class AS quote_asset_class,
trading_route_required AS trading_route_required,
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(trade_event_count) AS trade_event_count,
SUM(pair_candle_count) AS pair_candle_count
FROM classified
GROUP BY pair_trading_readiness, quote_asset_class, trading_route_required
ORDER BY
CASE pair_trading_readiness
WHEN 'direct_wsol_quote' THEN 1
WHEN 'direct_stable_quote' THEN 2
WHEN 'inverse_wsol_base' THEN 3
WHEN 'inverse_stable_base' THEN 4
WHEN 'cross_quote_requires_router' THEN 5
WHEN 'unknown_quote' THEN 6
ELSE 7
END,
pair_trading_readiness,
quote_asset_class
"#,
)
.fetch_all(pool)
.await;
let rows = match rows_result {
Ok(rows) => rows,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list local pair trading readiness diagnostic summaries on sqlite: {}",
error
)));
},
};
let mut summaries = std::vec::Vec::new();
for row in rows {
summaries.push(crate::LocalPairTradingReadinessDiagnosticSummaryDto {
pair_trading_readiness: row.pair_trading_readiness,
quote_asset_class: row.quote_asset_class,
trading_route_required: row.trading_route_required != 0,
pair_count: row.pair_count,
decoded_event_count: row.decoded_event_count,
decoded_trade_candidate_count: row.decoded_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.

View File

@@ -363,6 +363,8 @@ pub use db::LocalPairActionabilityDiagnosticSummaryDto;
pub use db::LocalPairDiagnosticSummaryDto;
/// Sample of a pair gap.
pub use db::LocalPairGapDiagnosticSampleDto;
/// Local pair diagnostics grouped by trading readiness class.
pub use db::LocalPairTradingReadinessDiagnosticSummaryDto;
/// Internal flat counter row for local diagnostics.
pub use db::LocalPipelineDiagnosticCountersDto;
/// Local pipeline diagnostics summary.
@@ -577,6 +579,8 @@ pub use db::query_local_non_actionable_pair_diagnostic_list_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 local pair trading-readiness summaries.
pub use db::query_local_pair_trading_readiness_diagnostic_list_summaries;
/// Lists samples of pairs without candles.
pub use db::query_local_pair_without_candle_diagnostic_list_samples;
/// Lists samples of pairs without trade events.

View File

@@ -42,6 +42,15 @@ impl LocalPipelineDiagnosticsService {
Ok(summaries) => summaries,
Err(error) => return Err(error),
};
let pair_trading_readiness_summaries_result =
crate::query_local_pair_trading_readiness_diagnostic_list_summaries(
self.database.as_ref(),
)
.await;
let pair_trading_readiness_summaries = match pair_trading_readiness_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;
@@ -182,6 +191,7 @@ impl LocalPipelineDiagnosticsService {
dex_summaries,
pair_summaries,
pair_actionability_summaries,
pair_trading_readiness_summaries,
decoded_event_summaries,
event_classification_summaries,
missing_trade_event_reason_summaries,

View File

@@ -42,6 +42,8 @@ pub struct LocalPipelineValidationConfig {
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,
/// Whether pair trading-readiness diagnostics must satisfy internal semantic invariants.
pub require_pair_trading_readiness_semantics: bool,
}
impl Default for LocalPipelineValidationConfig {
@@ -62,6 +64,7 @@ impl Default for LocalPipelineValidationConfig {
require_candles_per_dex: true,
require_no_non_actionable_trade_events_materialized: true,
require_dex_support_matrix_semantics: false,
require_pair_trading_readiness_semantics: false,
};
}
}
@@ -90,6 +93,7 @@ impl LocalPipelineValidationConfig {
require_candles_per_dex: true,
require_no_non_actionable_trade_events_materialized: true,
require_dex_support_matrix_semantics: false,
require_pair_trading_readiness_semantics: false,
};
}
@@ -123,6 +127,7 @@ impl LocalPipelineValidationConfig {
require_candles_per_dex: false,
require_no_non_actionable_trade_events_materialized: true,
require_dex_support_matrix_semantics: false,
require_pair_trading_readiness_semantics: false,
};
}
@@ -155,6 +160,7 @@ impl LocalPipelineValidationConfig {
require_candles_per_dex: false,
require_no_non_actionable_trade_events_materialized: true,
require_dex_support_matrix_semantics: false,
require_pair_trading_readiness_semantics: false,
};
}
@@ -191,6 +197,17 @@ impl LocalPipelineValidationConfig {
config.require_dex_support_matrix_semantics = true;
return config;
}
/// Builds the `0.7.33` pair trading-readiness validation config.
///
/// This profile keeps the `0.7.32` diagnostics semantics and requires all
/// persisted pairs to be covered by the pair trading-readiness summaries.
pub fn v0_7_33_pair_trading_readiness() -> Self {
let mut config = Self::v0_7_32_validation_report_semantics();
config.profile_code = "0.7.33_pair_trading_readiness".to_string();
config.require_pair_trading_readiness_semantics = true;
return config;
}
}
/// A single local pipeline validation issue.
@@ -343,6 +360,14 @@ impl LocalPipelineValidationService {
let config = crate::LocalPipelineValidationConfig::v0_7_32_validation_report_semantics();
return self.validate_current_database(&config).await;
}
/// Diagnoses the current database with the `0.7.33` pair trading-readiness profile.
pub async fn validate_v0_7_33_current_database(
&self,
) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
let config = crate::LocalPipelineValidationConfig::v0_7_33_pair_trading_readiness();
return self.validate_current_database(&config).await;
}
}
/// Validates a diagnostics summary without performing database access.
@@ -457,6 +482,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_pair_trading_readiness_semantics {
validate_pair_trading_readiness_semantics(&mut issues, summary);
}
if config.require_all_expected_dexes {
for expected_dex_code in &expected_dex_codes {
if !observed_dex_codes.contains(expected_dex_code) {
@@ -532,6 +560,109 @@ pub fn validate_local_pipeline_diagnostics_summary(
};
}
fn validate_pair_trading_readiness_semantics(
issues: &mut std::vec::Vec<crate::LocalPipelineValidationIssueDto>,
summary: &crate::LocalPipelineDiagnosticSummaryDto,
) {
if summary.pair_count > 0 && summary.pair_trading_readiness_summaries.is_empty() {
issues.push(crate::LocalPipelineValidationIssueDto {
code: "pair_trading_readiness_summary_missing".to_string(),
message: "pair trading-readiness summaries are missing".to_string(),
subject: None,
blocking: true,
});
return;
}
let mut readiness_pair_count = 0_i64;
let mut readiness_trade_materialized_pair_count = 0_i64;
let mut readiness_trade_event_count = 0_i64;
let mut readiness_pair_candle_count = 0_i64;
for readiness_summary in &summary.pair_trading_readiness_summaries {
if readiness_summary.pair_trading_readiness.trim().is_empty() {
issues.push(crate::LocalPipelineValidationIssueDto {
code: "pair_trading_readiness_empty".to_string(),
message: "pair trading-readiness class must not be empty".to_string(),
subject: None,
blocking: true,
});
}
if readiness_summary.pair_count <= 0 {
issues.push(crate::LocalPipelineValidationIssueDto {
code: "pair_trading_readiness_empty_group".to_string(),
message: format!(
"pair trading-readiness group '{}' has no pair",
readiness_summary.pair_trading_readiness
),
subject: Some(readiness_summary.pair_trading_readiness.clone()),
blocking: true,
});
}
readiness_pair_count += readiness_summary.pair_count;
readiness_trade_event_count += readiness_summary.trade_event_count;
readiness_pair_candle_count += readiness_summary.pair_candle_count;
if readiness_summary.pair_trading_readiness != "non_trade_materialized" {
readiness_trade_materialized_pair_count += readiness_summary.pair_count;
}
if readiness_summary.pair_trading_readiness == "unknown_quote"
&& readiness_summary.trade_event_count > 0
{
issues.push(crate::LocalPipelineValidationIssueDto {
code: "pair_trading_readiness_unknown_quote_materialized".to_string(),
message: format!(
"unknown quote readiness group has {} linked trade event(s)",
readiness_summary.trade_event_count
),
subject: Some(readiness_summary.pair_trading_readiness.clone()),
blocking: true,
});
}
}
if readiness_pair_count != summary.pair_count {
issues.push(crate::LocalPipelineValidationIssueDto {
code: "pair_trading_readiness_pair_count_mismatch".to_string(),
message: format!(
"pair trading-readiness summaries cover {} pair(s), expected {}",
readiness_pair_count, summary.pair_count
),
subject: None,
blocking: true,
});
}
if readiness_trade_materialized_pair_count != summary.trade_materialized_pair_count {
issues.push(crate::LocalPipelineValidationIssueDto {
code: "pair_trading_readiness_materialized_pair_count_mismatch".to_string(),
message: format!(
"pair trading-readiness materialized groups cover {} pair(s), expected {}",
readiness_trade_materialized_pair_count, summary.trade_materialized_pair_count
),
subject: None,
blocking: true,
});
}
if readiness_trade_event_count != summary.trade_event_count {
issues.push(crate::LocalPipelineValidationIssueDto {
code: "pair_trading_readiness_trade_event_count_mismatch".to_string(),
message: format!(
"pair trading-readiness summaries cover {} trade event(s), expected {}",
readiness_trade_event_count, summary.trade_event_count
),
subject: None,
blocking: true,
});
}
if readiness_pair_candle_count != summary.pair_candle_count {
issues.push(crate::LocalPipelineValidationIssueDto {
code: "pair_trading_readiness_candle_count_mismatch".to_string(),
message: format!(
"pair trading-readiness summaries cover {} candle bucket(s), expected {}",
readiness_pair_candle_count, summary.pair_candle_count
),
subject: None,
blocking: true,
});
}
}
fn validate_dex_support_matrix_semantics(
issues: &mut std::vec::Vec<crate::LocalPipelineValidationIssueDto>,
) {
@@ -728,6 +859,38 @@ mod tests {
pair_candle_count: 0,
},
],
pair_trading_readiness_summaries: vec![
crate::LocalPairTradingReadinessDiagnosticSummaryDto {
pair_trading_readiness: "direct_wsol_quote".to_string(),
quote_asset_class: "wsol".to_string(),
trading_route_required: false,
pair_count: 20,
decoded_event_count: 180,
decoded_trade_candidate_count: 180,
trade_event_count: 180,
pair_candle_count: 200,
},
crate::LocalPairTradingReadinessDiagnosticSummaryDto {
pair_trading_readiness: "direct_stable_quote".to_string(),
quote_asset_class: "stable".to_string(),
trading_route_required: false,
pair_count: 3,
decoded_event_count: 30,
decoded_trade_candidate_count: 30,
trade_event_count: 30,
pair_candle_count: 30,
},
crate::LocalPairTradingReadinessDiagnosticSummaryDto {
pair_trading_readiness: "non_trade_materialized".to_string(),
quote_asset_class: "other".to_string(),
trading_route_required: false,
pair_count: 4,
decoded_event_count: 0,
decoded_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![],
@@ -778,6 +941,11 @@ mod tests {
});
summary.pool_count = 95;
summary.pair_count = 95;
for readiness_summary in &mut summary.pair_trading_readiness_summaries {
if readiness_summary.pair_trading_readiness == "non_trade_materialized" {
readiness_summary.pair_count = 72;
}
}
return summary;
}
@@ -883,6 +1051,28 @@ mod tests {
assert_eq!(summary.blocking_pair_without_trade_count, 0);
}
#[test]
fn validation_accepts_0_7_33_pair_trading_readiness_summary() {
let summary = make_0_7_28_summary_with_meteora();
let config = crate::LocalPipelineValidationConfig::v0_7_33_pair_trading_readiness();
let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
assert!(report.validation_passed);
assert_eq!(report.validation_profile_code, "0.7.33_pair_trading_readiness");
assert_eq!(report.blocking_issue_count, 0);
}
#[test]
fn validation_rejects_0_7_33_pair_trading_readiness_mismatch() {
let mut summary = make_0_7_28_summary_with_meteora();
summary.pair_trading_readiness_summaries.retain(|readiness_summary| {
return readiness_summary.pair_trading_readiness != "non_trade_materialized";
});
let config = crate::LocalPipelineValidationConfig::v0_7_33_pair_trading_readiness();
let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
assert!(!report.validation_passed);
assert_eq!(report.issues[0].code, "pair_trading_readiness_pair_count_mismatch");
}
#[test]
fn validation_rejects_0_7_31_failed_transaction_trade_events() {
let mut summary = make_0_7_28_summary_with_meteora();