Spaces:
Running
Running
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.
|
| 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:
|
| 556 |
-
|
|
|
|
|
|
|
|
|
|
| 557 |
selected = []
|
| 558 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
for candidate in quality_passed:
|
| 560 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|