ifieryarrows commited on
Commit
8fdaddb
·
verified ·
1 Parent(s): a242cd4

Sync from GitHub (tests passed)

Browse files
screener/feature_screener/orchestrator.py CHANGED
@@ -467,7 +467,7 @@ class FeatureScreener:
467
  return {"selected": [], "limits_applied": {}}
468
 
469
  # Selection rules version (increment when limits/logic changes)
470
- SELECTION_RULES_VERSION = "3.1.0" # v3.1: + target redundancy filter
471
 
472
  # Global cap - hard limit on total selected
473
  MAX_SELECTED_TOTAL = 20
@@ -552,12 +552,52 @@ class FeatureScreener:
552
 
553
  quality_passed.append(candidate)
554
 
555
- # Step 2: Apply category limits (whitelist) + global cap
556
- counts = {}
 
 
 
557
  selected = []
558
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  for candidate in quality_passed:
560
- # Check global cap first
 
 
 
 
561
  if len(selected) >= MAX_SELECTED_TOTAL:
562
  excluded_reasons["global_cap_reached"] += 1
563
  continue
@@ -580,6 +620,7 @@ class FeatureScreener:
580
  entry = {
581
  "ticker": candidate.ticker,
582
  "category": category,
 
583
  "rank": candidate.decision.rank,
584
  "score_composite": candidate.decision.score_composite,
585
  # IS metrics
@@ -616,8 +657,12 @@ class FeatureScreener:
616
  # Candidate counts
617
  "total_candidates": len(self.output.candidates),
618
  "passed_oos_quality_gate": len(quality_passed),
 
 
619
  "selected_count": len(selected),
620
  # Gate/limit configs
 
 
621
  "max_selected_total": MAX_SELECTED_TOTAL,
622
  "oos_quality_gates": oos_quality_gates,
623
  "category_limits": category_limits,
 
467
  return {"selected": [], "limits_applied": {}}
468
 
469
  # Selection rules version (increment when limits/logic changes)
470
+ SELECTION_RULES_VERSION = "3.2.0" # v3.2: + mandatory core drivers
471
 
472
  # Global cap - hard limit on total selected
473
  MAX_SELECTED_TOTAL = 20
 
552
 
553
  quality_passed.append(candidate)
554
 
555
+ # Step 2: Add MANDATORY core drivers first (always included)
556
+ # These are known to be predictive regardless of correlation screening
557
+ MANDATORY_SYMBOLS = ["DX-Y.NYB", "CL=F"] # USD, Oil
558
+
559
+ mandatory_added = []
560
  selected = []
561
 
562
+ # Find mandatory symbols in candidates (any candidate, not just quality_passed)
563
+ all_candidates_by_ticker = {c.ticker: c for c in self.output.candidates}
564
+
565
+ for ticker in MANDATORY_SYMBOLS:
566
+ if ticker in all_candidates_by_ticker:
567
+ candidate = all_candidates_by_ticker[ticker]
568
+ oos = candidate.oos_metrics
569
+ category = candidate.category or "other"
570
+ entry = {
571
+ "ticker": candidate.ticker,
572
+ "category": category,
573
+ "selection_source": "mandatory", # Audit: how this was selected
574
+ "rank": candidate.decision.rank if candidate.decision else None,
575
+ "score_composite": candidate.decision.score_composite if candidate.decision else None,
576
+ # IS metrics
577
+ "is_pearson": candidate.is_metrics.pearson if candidate.is_metrics else None,
578
+ "is_n_obs": candidate.is_metrics.n_obs if candidate.is_metrics else None,
579
+ "is_best_lag": candidate.is_metrics.best_lead_lag if candidate.is_metrics else None,
580
+ # OOS metrics
581
+ "oos_pearson": oos.pearson if oos else None,
582
+ "oos_pearson_sign": "positive" if (oos and oos.pearson and oos.pearson >= 0) else "negative",
583
+ "oos_n_obs": oos.n_obs if oos else None,
584
+ "oos_partial_corr": oos.partial_corr if oos else None,
585
+ "oos_rolling_std": oos.rolling_corr_std if oos else None,
586
+ "oos_frozen_lag": oos.frozen_lag if oos else None,
587
+ "oos_lag_corr_at_frozen": oos.lag_corr_at_frozen if oos else None,
588
+ }
589
+ selected.append(entry)
590
+ mandatory_added.append(ticker)
591
+
592
+ # Step 3: Apply category limits (whitelist) + global cap for screener-selected
593
+ counts = {}
594
+
595
  for candidate in quality_passed:
596
+ # Skip if already added as mandatory
597
+ if candidate.ticker in mandatory_added:
598
+ continue
599
+
600
+ # Check global cap (accounting for mandatory already added)
601
  if len(selected) >= MAX_SELECTED_TOTAL:
602
  excluded_reasons["global_cap_reached"] += 1
603
  continue
 
620
  entry = {
621
  "ticker": candidate.ticker,
622
  "category": category,
623
+ "selection_source": "screener", # Audit: selected by screener
624
  "rank": candidate.decision.rank,
625
  "score_composite": candidate.decision.score_composite,
626
  # IS metrics
 
657
  # Candidate counts
658
  "total_candidates": len(self.output.candidates),
659
  "passed_oos_quality_gate": len(quality_passed),
660
+ "mandatory_count": len(mandatory_added),
661
+ "screener_selected_count": len(selected) - len(mandatory_added),
662
  "selected_count": len(selected),
663
  # Gate/limit configs
664
+ "mandatory_symbols": MANDATORY_SYMBOLS,
665
+ "mandatory_added": mandatory_added,
666
  "max_selected_total": MAX_SELECTED_TOTAL,
667
  "oos_quality_gates": oos_quality_gates,
668
  "category_limits": category_limits,