diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 20fcabf..0b9a982 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -9219,6 +9219,15 @@ impl PlatformExprStrategy { .unwrap_or_else(|| self.field_value(candidate))) } + fn rank_reuses_market_cap_order(&self) -> bool { + self.config.rank_expr.trim().is_empty() + && !self.config.rank_desc + && matches!( + self.config.rank_by.trim(), + "market_cap" | "market_cap_bn" + ) + } + fn can_sell_position(&self, ctx: &StrategyContext<'_>, date: NaiveDate, symbol: &str) -> bool { self.can_sell_position_at_time(ctx, date, symbol, None) } @@ -9429,24 +9438,26 @@ impl PlatformExprStrategy { } candidates.push((candidate, stock, rank_value)); } - candidates.sort_by(|lhs, rhs| { - let lhs_value = lhs.2; - let rhs_value = rhs.2; - let ordering = if self.config.rank_desc { - rhs_value - .partial_cmp(&lhs_value) - .unwrap_or(std::cmp::Ordering::Equal) - } else { - lhs_value - .partial_cmp(&rhs_value) - .unwrap_or(std::cmp::Ordering::Equal) - }; - if ordering == std::cmp::Ordering::Equal { - lhs.0.symbol.cmp(&rhs.0.symbol) - } else { - ordering - } - }); + if !self.rank_reuses_market_cap_order() { + candidates.sort_by(|lhs, rhs| { + let lhs_value = lhs.2; + let rhs_value = rhs.2; + let ordering = if self.config.rank_desc { + rhs_value + .partial_cmp(&lhs_value) + .unwrap_or(std::cmp::Ordering::Equal) + } else { + lhs_value + .partial_cmp(&rhs_value) + .unwrap_or(std::cmp::Ordering::Equal) + }; + if ordering == std::cmp::Ordering::Equal { + lhs.0.symbol.cmp(&rhs.0.symbol) + } else { + ordering + } + }); + } let mut selected = Vec::new(); for (candidate, stock, _) in candidates { @@ -13749,6 +13760,28 @@ mod tests { ); } + #[test] + fn market_cap_ascending_rank_reuses_only_equivalent_universe_order() { + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.rank_by = "market_cap".to_string(); + let strategy = PlatformExprStrategy::new(cfg.clone()); + assert!(strategy.rank_reuses_market_cap_order()); + + cfg.rank_by = "market_cap_bn".to_string(); + assert!(PlatformExprStrategy::new(cfg.clone()).rank_reuses_market_cap_order()); + + cfg.rank_desc = true; + assert!(!PlatformExprStrategy::new(cfg.clone()).rank_reuses_market_cap_order()); + + cfg.rank_desc = false; + cfg.rank_expr = "close".to_string(); + assert!(!PlatformExprStrategy::new(cfg.clone()).rank_reuses_market_cap_order()); + + cfg.rank_expr.clear(); + cfg.rank_by = "free_float_cap".to_string(); + assert!(!PlatformExprStrategy::new(cfg).rank_reuses_market_cap_order()); + } + #[test] fn current_rolling_helpers_do_not_load_factor_maps_or_decision_rollings() { let mut cfg = PlatformExprStrategyConfig::microcap_rotation();