diff --git a/crates/fidc-core/src/market_event_context.rs b/crates/fidc-core/src/market_event_context.rs index e6a0d3d..32b7b51 100644 --- a/crates/fidc-core/src/market_event_context.rs +++ b/crates/fidc-core/src/market_event_context.rs @@ -67,6 +67,7 @@ pub struct OutputDay { pub no_limit: usize, pub profit_effect_members: Vec, pub profit_effect_missing: Vec, + pub industry_missing: Vec, } #[derive(Serialize)] @@ -193,8 +194,14 @@ pub fn aggregate(request: Request) -> Result { fields.insert("industry_rank".into(), ranks.get(name).copied()); fields.insert("industry_size".into(), Some(universe.len() as f64)); } + let industry_missing=members.iter().filter(|(_,group)|group.is_none()).map(|(s,_)|s.clone()).collect::>(); + if !industry_missing.is_empty() { + // An unclassified member may belong to any group; never silently shrink a group. + state.industry_history.clear(); + for fields in industries.values_mut() { for value in fields.values_mut() { *value=None; } } + } output.push(OutputDay { date:day.date, common, industries, members, securities:day.rows.len(), active, paused, - no_limit:unlimited, profit_effect_members:previous_ups, profit_effect_missing:profit_missing }); + no_limit:unlimited, profit_effect_members:previous_ups, profit_effect_missing:profit_missing, industry_missing }); state.last_date = Some(day.date); state.streaks = streaks; state.limit_ups = ups; } Ok(Output {contract:CONTRACT, days:output, state}) @@ -236,4 +243,13 @@ mod tests { let unknown=aggregate(Request{days:vec![day(1,true)],previous:State::default()}).unwrap(); assert_eq!(unknown.days[0].common["market_high_board"],None); } + #[test] + fn missing_industry_does_not_invent_groups_or_disable_independent_market_facts() { + let mut missing=day(2,true);missing.rows[0].industry=None; + let r=aggregate(Request{days:vec![day(1,false),missing,day(3,true)],previous:State::default()}).unwrap(); + assert_eq!(r.days[1].common["market_breadth"],Some(0.5)); + assert_eq!(r.days[1].industry_missing,vec!["A"]); + assert!(r.days[1].industries.values().flat_map(|g|g.values()).all(Option::is_none)); + assert_eq!(r.days[2].industries["I"]["industry_return_20"],None); + } }