@@ -211,6 +211,9 @@ class MarketConfirmation:
211211 price_observation_count : int = 0
212212 confirmation_quality : str = ""
213213 warnings : str = ""
214+ compatibility_used : bool = False
215+ compatibility_reason : str = ""
216+ compatibility_provenance : str = ""
214217
215218
216219def parse_date (value : str ) -> dt .date :
@@ -316,13 +319,23 @@ def apply_input_freshness(
316319 warnings .append ("theme_momentum_invalid_theme_ranks" )
317320 theme_momentum = None
318321 return ai_signal , theme_momentum , warnings
319- if any (
320- not isinstance (theme , dict ) or not isinstance (theme .get ("top_symbols" ), list )
321- for theme in theme_ranks
322- ):
322+ if any (not isinstance (theme , dict ) or not isinstance (theme .get ("top_symbols" ), list ) for theme in theme_ranks ):
323323 warnings .append ("theme_momentum_invalid_top_symbols" )
324324 theme_momentum = None
325325 return ai_signal , theme_momentum , warnings
326+ sanitized_ranks : list [dict [str , Any ]] = []
327+ invalid_symbol_count = 0
328+ for theme in theme_ranks :
329+ symbols = []
330+ for item in theme ["top_symbols" ]:
331+ if isinstance (item , dict ) and str (item .get ("symbol" , "" )).strip ():
332+ symbols .append (item )
333+ else :
334+ invalid_symbol_count += 1
335+ sanitized_ranks .append ({** theme , "top_symbols" : symbols })
336+ if invalid_symbol_count :
337+ warnings .append (f"theme_momentum_symbols_excluded:{ invalid_symbol_count } " )
338+ theme_momentum = {** theme_momentum , "theme_ranks" : sanitized_ranks }
326339 extreme = any (
327340 abs (as_float (item .get ("return_3m" ))) > 2.0
328341 for theme in theme_ranks
@@ -408,7 +421,9 @@ def optional_date(value: Any) -> dt.date | None:
408421 return parse_date (text )
409422
410423
411- def load_market_confirmation (path : str | Path | None , as_of : dt .date ) -> dict [str , MarketConfirmation ]:
424+ def load_market_confirmation (
425+ path : str | Path | None , as_of : dt .date , * , compatibility_mode : bool = False
426+ ) -> dict [str , MarketConfirmation ]:
412427 if path is None :
413428 return {}
414429 confirmations : dict [str , MarketConfirmation ] = {}
@@ -423,6 +438,20 @@ def load_market_confirmation(path: str | Path | None, as_of: dt.date) -> dict[st
423438 if current and current .as_of and row_as_of and row_as_of < current .as_of :
424439 continue
425440 quality_metadata_present = "confirmation_quality" in row and "warnings" in row
441+ compatibility_used = False
442+ compatibility_reason = ""
443+ compatibility_provenance = ""
444+ reconstructed_quality = ""
445+ if not quality_metadata_present and compatibility_mode :
446+ compatibility_used = True
447+ compatibility_reason = "legacy_csv_quality_reconstructed"
448+ compatibility_provenance = "legacy_csv_fields:as_of,return_63d,data_source,price_age_days"
449+ if row_as_of is None or (as_of - row_as_of ).days > 7 :
450+ reconstructed_quality = "stale_price"
451+ elif abs (as_float (row .get ("return_63d" ))) > 2.0 :
452+ reconstructed_quality = "anomalous"
453+ else :
454+ reconstructed_quality = "price_observed"
426455 confirmations [symbol ] = MarketConfirmation (
427456 symbol = symbol ,
428457 as_of = row_as_of ,
@@ -439,8 +468,12 @@ def load_market_confirmation(path: str | Path | None, as_of: dt.date) -> dict[st
439468 price_observation_count = int (as_float (row .get ("price_observation_count" ))),
440469 confirmation_quality = (str (row .get ("confirmation_quality" , "" )).strip () or "missing_quality_metadata" )
441470 if quality_metadata_present
442- else "missing_quality_metadata" ,
443- warnings = str (row .get ("warnings" , "" )),
471+ else reconstructed_quality or "missing_quality_metadata" ,
472+ warnings = str (row .get ("warnings" , "" ))
473+ or ("legacy_market_confirmation_compatibility_used" if compatibility_used else "" ),
474+ compatibility_used = compatibility_used ,
475+ compatibility_reason = compatibility_reason ,
476+ compatibility_provenance = compatibility_provenance ,
444477 )
445478 return confirmations
446479
@@ -1063,6 +1096,8 @@ def market_confirmation_score(market: MarketConfirmation | None) -> float | None
10631096def market_quality_warnings (confirmations : dict [str , MarketConfirmation ]) -> list [str ]:
10641097 warnings : list [str ] = []
10651098 for symbol , market in sorted (confirmations .items ()):
1099+ if market .compatibility_used :
1100+ warnings .append (f"market_confirmation_compatibility_used:{ symbol } :{ market .compatibility_reason } " )
10661101 if market .confirmation_quality == "price_observed" :
10671102 continue
10681103 quality = market .confirmation_quality or "missing_quality_metadata"
@@ -1520,6 +1555,7 @@ def build_advisory_report(
15201555 theme_momentum_path : str | Path | None = None ,
15211556 market_confirmation_path : str | Path | None = None ,
15221557 max_candidates : int = 12 ,
1558+ market_compatibility_mode : bool = False ,
15231559) -> dict [str , Any ]:
15241560 if cadence not in ALLOWED_CADENCES :
15251561 raise ValueError (f"cadence must be one of: { ', ' .join (sorted (ALLOWED_CADENCES ))} " )
@@ -1531,7 +1567,9 @@ def build_advisory_report(
15311567 ai_signal , theme_momentum , freshness_warnings = apply_input_freshness (
15321568 ai_signal = ai_signal , theme_momentum = theme_momentum , as_of = as_of_date
15331569 )
1534- market_confirmations = load_market_confirmation (market_confirmation_path , as_of_date )
1570+ market_confirmations = load_market_confirmation (
1571+ market_confirmation_path , as_of_date , compatibility_mode = market_compatibility_mode
1572+ )
15351573 theme_momentum_summary = summarize_theme_momentum (theme_momentum )
15361574 source_mode , data_quality_warnings = source_mode_for_paths (
15371575 political_events_path ,
@@ -1598,6 +1636,18 @@ def build_advisory_report(
15981636 "top_theme_ids" : [theme ["theme_id" ] for theme in theme_momentum_summary ["top_themes" ]],
15991637 "theme_first_candidate_count" : len (theme_first_candidates ),
16001638 "market_confirmation_count" : len (market_confirmations ),
1639+ "market_confirmation_compatibility_used" : any (
1640+ item .compatibility_used for item in market_confirmations .values ()
1641+ ),
1642+ "market_confirmation_compatibility" : [
1643+ {
1644+ "symbol" : symbol ,
1645+ "reason" : item .compatibility_reason ,
1646+ "provenance" : item .compatibility_provenance ,
1647+ }
1648+ for symbol , item in sorted (market_confirmations .items ())
1649+ if item .compatibility_used
1650+ ],
16011651 "long_context_available" : bool (long_context_symbols ),
16021652 "long_context_symbol_count" : len (long_context_symbols ),
16031653 "long_context_symbols" : long_context_symbols [:12 ],
@@ -1749,6 +1799,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
17491799 parser .add_argument ("--ai-signal" , help = "Saved AI shadow signal JSON." )
17501800 parser .add_argument ("--theme-momentum" , help = "Saved theme momentum snapshot JSON." )
17511801 parser .add_argument ("--market-confirmation" , help = "Optional point-in-time market confirmation CSV." )
1802+ parser .add_argument ("--market-compatibility-mode" , action = "store_true" , help = "Explicit historical/replay mode for legacy market CSVs." )
17521803 parser .add_argument ("--max-items" , "--max-candidates" , dest = "max_candidates" , type = int , default = 12 )
17531804 parser .add_argument ("--output-json" , required = True , help = "Output JSON artifact path." )
17541805 parser .add_argument ("--output-md" , required = True , help = "Output Markdown report path." )
@@ -1766,6 +1817,7 @@ def main(argv: list[str] | None = None) -> None:
17661817 ai_signal_path = args .ai_signal ,
17671818 theme_momentum_path = args .theme_momentum ,
17681819 market_confirmation_path = args .market_confirmation ,
1820+ market_compatibility_mode = args .market_compatibility_mode ,
17691821 max_candidates = args .max_candidates ,
17701822 )
17711823 write_json (args .output_json , report )
0 commit comments