From 9bc525a264eec1e141ce445d5364184b18bad75f Mon Sep 17 00:00:00 2001 From: TPTBusiness Date: Fri, 1 May 2026 15:58:01 +0200 Subject: [PATCH] feat(optimizer): add max_positions parameter to Optuna search space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add max_positions (1-5) as an optimizable hyperparameter across all three Optuna search stages (coarse, fine, very fine). The parameter scales effective position size as min(position_size_pct × max_positions, 1.0), allowing the optimizer to discover pyramiding strategies. Co-Authored-By: Claude Sonnet 4.6 --- rdagent/components/coder/optuna_optimizer.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/rdagent/components/coder/optuna_optimizer.py b/rdagent/components/coder/optuna_optimizer.py index 3268b64e..fe39e179 100644 --- a/rdagent/components/coder/optuna_optimizer.py +++ b/rdagent/components/coder/optuna_optimizer.py @@ -292,6 +292,7 @@ class OptunaOptimizer: "volatility_lookback": trial.suggest_int("volatility_lookback", 5, 500, step=5), "signal_bias": trial.suggest_float("signal_bias", -1.0, 1.0, step=0.05), "max_hold_bars": trial.suggest_int("max_hold_bars", 5, 1000, step=5), + "max_positions": trial.suggest_int("max_positions", 1, 5, step=1), } # Parameters that are allowed to be negative (not clamped to 0). @@ -308,6 +309,7 @@ class OptunaOptimizer: "volatility_lookback": 1.0, "signal_bias": -1.0, "max_hold_bars": 1.0, + "max_positions": 1.0, } def _suggest_bounded( @@ -357,6 +359,7 @@ class OptunaOptimizer: "volatility_lookback": (center.get("volatility_lookback", 100), 30), "signal_bias": (center.get("signal_bias", 0.0), 0.2), "max_hold_bars": (center.get("max_hold_bars", 100), 50), + "max_positions": (center.get("max_positions", 1), 2), } return {key: self._suggest_bounded(trial, key, c, hw) for key, (c, hw) in ranges.items()} @@ -388,6 +391,7 @@ class OptunaOptimizer: "volatility_lookback": (center.get("volatility_lookback", 100), 10), "signal_bias": (center.get("signal_bias", 0.0), 0.07), "max_hold_bars": (center.get("max_hold_bars", 100), 17), + "max_positions": (center.get("max_positions", 1), 1), } return {key: self._suggest_bounded(trial, key, c, hw) for key, (c, hw) in ranges.items()} @@ -467,6 +471,9 @@ class OptunaOptimizer: # Max holding periods (in bars) "max_hold_bars": trial.suggest_int("max_hold_bars", 10, 500, step=10), + + # Max concurrent positions (1 = no pyramiding, 2-5 = scale-in) + "max_positions": trial.suggest_int("max_positions", 1, 5, step=1), } return params @@ -597,6 +604,13 @@ class OptunaOptimizer: if signal_bias != 0.0: signal = (signal.astype(float) + signal_bias).round().astype(int).clip(-1, 1) + # Apply max_positions: scale signal by position_size_pct and cap exposure + max_positions = int(params.get("max_positions", 1)) + position_size_pct = float(params.get("position_size_pct", 1.0)) + # Each "position" is position_size_pct of equity; total exposure capped at max_positions × size + effective_size = min(position_size_pct * max_positions, 1.0) + signal = (signal.astype(float) * effective_size).clip(-1.0, 1.0) + # Build a synthetic close from the factor-mean so we can route # through the same unified engine as every other backtest path. # Backtest formulas must match the orchestrator's real-OHLCV path.