Skip to content

Commit 65dce39

Browse files
committed
fix(ml): fix optimizer crash and remove IC fallback gate
- optimizer.py: use skfolio's LedoitWolf (BaseCovariance) instead of sklearn's, which caused TypeError in skfolio v0.15+ and silently fell back to equal-weight HRP - predict.py: remove IC quality gate fallback to equal-weight — low IC still contains directional signal; HRP diversifies away noise - model.py: reduce LightGBM min_child_samples 250→50 (was severely underfitting cross-sectional equity)
1 parent 892a9e8 commit 65dce39

3 files changed

Lines changed: 10 additions & 16 deletions

File tree

python/alpha/model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def _default_params(self) -> dict:
3434
"metric": "huber",
3535
"learning_rate": 0.05,
3636
"num_leaves": 31,
37-
"min_child_samples": 250,
37+
"min_child_samples": 50,
3838
"subsample": 0.7,
3939
"colsample_bytree": 0.7,
4040
"verbose": -1,

python/alpha/predict.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -867,21 +867,16 @@ def get_ml_weights(
867867
else:
868868
raise RuntimeError(f"Training failed and no cached model available: {e}") from e
869869

870-
# IC quality gate: if the model's validation IC is too low, fall back
871-
# to equal-weight portfolio. A weak model is worse than no model.
870+
# IC quality gate: log warning but proceed with the model's predictions.
871+
# Previously this fell back to equal-weight, which defeated the purpose
872+
# of having an ML pipeline. Low IC still contains directional signal;
873+
# the HRP optimizer downstream will diversify away noise.
872874
model_ic = getattr(model, "validation_ic", None)
873875
if isinstance(model_ic, (int, float)) and model_ic < MIN_VALIDATION_IC:
874876
logger.warning(
875877
f"Model validation IC ({model_ic:.4f}) below minimum "
876-
f"({MIN_VALIDATION_IC}). Falling back to equal-weight."
878+
f"({MIN_VALIDATION_IC}). Proceeding with model predictions anyway."
877879
)
878-
# Return equal-weight across top_n current holdings if available,
879-
# otherwise return empty (no trades).
880-
if current_weights:
881-
tickers = list(current_weights.keys())[:top_n]
882-
eq_wt = 1.0 / len(tickers) if tickers else 0.0
883-
return {t: eq_wt for t in tickers}, stale_data
884-
return {}, stale_data
885880

886881
# Step 2: Fetch recent data for the full universe to rank
887882
logger.info("Step 2/4: Fetching recent data for universe ranking...")

python/portfolio/optimizer.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,14 @@ def __init__(
9595
def _prior_estimator(self):
9696
"""Return an EmpiricalPrior with Ledoit-Wolf covariance if available (R3-O-1).
9797
98-
Creates a fresh LedoitWolf instance each time instead of reusing the
99-
pre-fitted one, so skfolio's internal fit() produces correct results
100-
regardless of version-specific assumptions about estimator state.
98+
Uses skfolio's own LedoitWolf (inherits BaseCovariance) — NOT sklearn's,
99+
which causes TypeError in skfolio v0.15+.
101100
"""
102101
if self._shrunk:
103102
try:
104-
from sklearn.covariance import LedoitWolf
103+
from skfolio.moments.covariance import LedoitWolf as SkfolioLW
105104

106-
return EmpiricalPrior(covariance_estimator=LedoitWolf())
105+
return EmpiricalPrior(covariance_estimator=SkfolioLW())
107106
except Exception as e:
108107
logger.warning(f"Could not create shrunk prior: {e}")
109108
return EmpiricalPrior()

0 commit comments

Comments
 (0)