diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 2d6159d..c6b4ecf 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -9214,6 +9214,17 @@ impl PlatformExprStrategy { .0 } + #[inline] + fn eligible_universe_market_cap_order( + left: &EligibleUniverseSnapshot, + right: &EligibleUniverseSnapshot, + ) -> std::cmp::Ordering { + left.market_cap_bn + .partial_cmp(&right.market_cap_bn) + .unwrap_or(std::cmp::Ordering::Equal) + .then_with(|| left.symbol.cmp(&right.symbol)) + } + fn selection_universe_and_risk_decisions_with_options( &self, ctx: &StrategyContext<'_>, @@ -9296,12 +9307,7 @@ impl PlatformExprStrategy { free_float_cap_bn, }); } - rows.sort_by(|left, right| { - left.market_cap_bn - .partial_cmp(&right.market_cap_bn) - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| left.symbol.cmp(&right.symbol)) - }); + rows.sort_unstable_by(Self::eligible_universe_market_cap_order); (rows, decisions) } @@ -14256,6 +14262,36 @@ mod tests { assert!(!PlatformExprStrategy::new(cfg).rank_reuses_market_cap_order()); } + #[test] + fn eligible_universe_unstable_sort_preserves_market_cap_symbol_total_order() { + let mut rows = vec![ + EligibleUniverseSnapshot { + symbol: "000003.SZ".to_string(), + market_cap_bn: 2.0, + free_float_cap_bn: 1.0, + }, + EligibleUniverseSnapshot { + symbol: "000002.SZ".to_string(), + market_cap_bn: 1.0, + free_float_cap_bn: 1.0, + }, + EligibleUniverseSnapshot { + symbol: "000001.SZ".to_string(), + market_cap_bn: 1.0, + free_float_cap_bn: 1.0, + }, + ]; + + rows.sort_unstable_by(PlatformExprStrategy::eligible_universe_market_cap_order); + + assert_eq!( + rows.iter() + .map(|row| (row.market_cap_bn, row.symbol.as_str())) + .collect::>(), + vec![(1.0, "000001.SZ"), (1.0, "000002.SZ"), (2.0, "000003.SZ"),] + ); + } + #[test] fn current_rolling_helpers_do_not_load_factor_maps_or_decision_rollings() { let mut cfg = PlatformExprStrategyConfig::microcap_rotation();