From 15084f593c54aab1b7d4178c136a3b338e97f41b Mon Sep 17 00:00:00 2001 From: TPTBusiness Date: Sat, 2 May 2026 23:21:38 +0200 Subject: [PATCH] fix: resolve dead code, shell injection risk, mutable defaults, and other bugs - strategy_orchestrator.py: remove unreachable dead 'if not factor_values' after early return - strategy_orchestrator.py: eliminate duplicate OHLVC load in evaluate_strategy - env.py: escape single-quotes in Docker entry to prevent shell injection (CWE-78) - env.py: replace mutable default args with None pattern in DockerEnv subclasses - factor_runner.py: move pandarallel.initialize() from import-time to lazy init --- .../components/coder/strategy_orchestrator.py | 28 +- .../scenarios/qlib/developer/factor_runner.py | 268 +++++++++--------- rdagent/utils/env.py | 27 +- 3 files changed, 156 insertions(+), 167 deletions(-) diff --git a/rdagent/components/coder/strategy_orchestrator.py b/rdagent/components/coder/strategy_orchestrator.py index 20da333f..e679e795 100644 --- a/rdagent/components/coder/strategy_orchestrator.py +++ b/rdagent/components/coder/strategy_orchestrator.py @@ -749,18 +749,15 @@ signal = signal.rolling(window=3, min_periods=1).mean().round().astype(int) } # Align factor values with common index - if not factor_values: - df_factors = pd.DataFrame() - else: - # Find common index across all series - common_idx = None - for name, s in factor_values.items(): - if common_idx is None: - common_idx = s.index - else: - common_idx = common_idx.intersection(s.index) + # Find common index across all series + common_idx = None + for name, s in factor_values.items(): + if common_idx is None: + common_idx = s.index + else: + common_idx = common_idx.intersection(s.index) - if common_idx is not None and len(common_idx) > 100: + if common_idx is not None and len(common_idx) > 100: df_factors = pd.DataFrame({ name: s.reindex(common_idx) for name, s in factor_values.items() }).dropna() @@ -826,16 +823,15 @@ signal = signal.rolling(window=3, min_periods=1).mean().round().astype(int) "factors_used": factor_names, } - if "signal" not in local_vars: + signal = local_vars.get("signal") + if signal is None or (isinstance(signal, pd.Series) and signal.empty): return { "strategy_name": strategy_name, "status": "rejected", - "reason": "Strategy did not produce 'signal' variable", + "reason": "Strategy did not produce valid 'signal' variable", "factors_used": factor_names, } - signal = local_vars["signal"] - logger.info( f"[DEBUG] {strategy_name}: signal stats: " f"len={len(signal)}, " @@ -852,7 +848,7 @@ signal = signal.rolling(window=3, min_periods=1).mean().round().astype(int) backtest_signal_ftmo, ) - close = self.load_ohlcv_close() + # Reuse the already-loaded close from above; create a synthetic proxy if unavailable if close is None: logger.warning("OHLCV data unavailable, using factor-mean proxy") proxy = df_factors.mean(axis=1).astype(float) diff --git a/rdagent/scenarios/qlib/developer/factor_runner.py b/rdagent/scenarios/qlib/developer/factor_runner.py index b23029bf..d544c134 100644 --- a/rdagent/scenarios/qlib/developer/factor_runner.py +++ b/rdagent/scenarios/qlib/developer/factor_runner.py @@ -1,7 +1,7 @@ -import sys -import os import logging +import os from pathlib import Path + """ Qlib Factor Runner - Executes factor backtests in Docker. @@ -11,15 +11,8 @@ NOTE: The @cache_with_pickle decorator was REMOVED from develop() because: - Docker-level caching (QlibDockerConf.enable_cache=False) is sufficient - The pickle cache caused 240+ factor generations but ZERO Docker backtests """ -from pathlib import Path -from typing import Optional import pandas as pd -from pandarallel import pandarallel - - -pandarallel.initialize(verbose=1) - from rdagent.app.qlib_rd_loop.conf import FactorBasePropSetting from rdagent.components.runner import CachedRunner from rdagent.core.exception import FactorEmptyError @@ -74,7 +67,7 @@ def _shift_daily_constant_factor_if_needed(factor_col: "pd.Series", factor_name: logger.warning( f"[LookAheadFix] Factor '{factor_name}' is daily-constant " - f"({fraction_constant:.0%} of days). Applying 1-day shift to remove look-ahead bias." + f"({fraction_constant:.0%} of days). Applying 1-day shift to remove look-ahead bias.", ) # Shift: for each instrument, map daily values forward by 1 trading day @@ -122,13 +115,13 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): """ def calculate_information_coefficient( - self, concat_feature: pd.DataFrame, SOTA_feature_column_size: int, new_feature_columns_size: int + self, concat_feature: pd.DataFrame, SOTA_feature_column_size: int, new_feature_columns_size: int, ) -> pd.DataFrame: res = pd.Series(index=range(SOTA_feature_column_size * new_feature_columns_size)) for col1 in range(SOTA_feature_column_size): for col2 in range(SOTA_feature_column_size, SOTA_feature_column_size + new_feature_columns_size): res.loc[col1 * new_feature_columns_size + col2 - SOTA_feature_column_size] = concat_feature.iloc[ - :, col1 + :, col1, ].corr(concat_feature.iloc[:, col2]) return res @@ -137,11 +130,14 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): # if the IC is larger than a threshold, remove the new_feature column # return the new_feature + from pandarallel import pandarallel + pandarallel.initialize(verbose=1) + concat_feature = pd.concat([SOTA_feature, new_feature], axis=1) IC_max = ( concat_feature.groupby("datetime") .parallel_apply( - lambda x: self.calculate_information_coefficient(x, SOTA_feature.shape[1], new_feature.shape[1]) + lambda x: self.calculate_information_coefficient(x, SOTA_feature.shape[1], new_feature.shape[1]), ) .mean() ) @@ -161,7 +157,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): self._ensure_results_dirs() if exp.based_experiments and exp.based_experiments[-1].result is None: - logger.info(f"Baseline experiment execution ...") + logger.info("Baseline experiment execution ...") exp.based_experiments[-1] = self.develop(exp.based_experiments[-1]) fbps = FactorBasePropSetting() @@ -185,11 +181,11 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): base_exp for base_exp in exp.based_experiments if isinstance(base_exp, QlibFactorExperiment) ] if len(sota_factor_experiments_list) > 1: - logger.info(f"SOTA factor processing ...") + logger.info("SOTA factor processing ...") SOTA_factor = process_factor_data(sota_factor_experiments_list) # Process the new factors data - logger.info(f"New factor processing ...") + logger.info("New factor processing ...") new_factors = process_factor_data(exp) if new_factors.empty: @@ -200,7 +196,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): new_factors = self.deduplicate_new_factors(SOTA_factor, new_factors) if new_factors.empty: raise FactorEmptyError( - "The factors generated in this round are highly similar to the previous factors. Please change the direction for creating new factors." + "The factors generated in this round are highly similar to the previous factors. Please change the direction for creating new factors.", ) combined_factors = pd.concat([SOTA_factor, new_factors], axis=1).dropna() else: @@ -211,7 +207,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): combined_factors = combined_factors.loc[:, ~combined_factors.columns.duplicated(keep="last")] new_columns = pd.MultiIndex.from_product([["feature"], combined_factors.columns]) combined_factors.columns = new_columns - logger.info(f"Factor data processing completed.") + logger.info("Factor data processing completed.") num_features = len(exp.base_features) + len(combined_factors.columns) @@ -230,10 +226,10 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): sota_model_exp = base_exp exist_sota_model_exp = True break - logger.info(f"Experiment execution ...") + logger.info("Experiment execution ...") if exist_sota_model_exp: exp.experiment_workspace.inject_files( - **{"model.py": sota_model_exp.sub_workspace_list[0].file_dict["model.py"]} + **{"model.py": sota_model_exp.sub_workspace_list[0].file_dict["model.py"]}, ) sota_training_hyperparameters = sota_model_exp.sub_tasks[0].training_hyperparameters if sota_training_hyperparameters: @@ -244,19 +240,19 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): "early_stop": str(sota_training_hyperparameters.get("early_stop", 10)), "batch_size": str(sota_training_hyperparameters.get("batch_size", 256)), "weight_decay": str(sota_training_hyperparameters.get("weight_decay", 0.0001)), - } + }, ) sota_model_type = sota_model_exp.sub_tasks[0].model_type if sota_model_type == "TimeSeries": env_to_use.update( - {"dataset_cls": "TSDatasetH", "num_features": num_features, "step_len": 20, "num_timesteps": 20} + {"dataset_cls": "TSDatasetH", "num_features": num_features, "step_len": 20, "num_timesteps": 20}, ) elif sota_model_type == "Tabular": env_to_use.update({"dataset_cls": "DatasetH", "num_features": num_features}) # model + combined factors result, stdout = exp.experiment_workspace.execute( - qlib_config_name="conf_combined_factors_sota_model.yaml", run_env=env_to_use + qlib_config_name="conf_combined_factors_sota_model.yaml", run_env=env_to_use, ) else: # LGBM + combined factors @@ -265,7 +261,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): run_env=env_to_use, ) else: - logger.info(f"Experiment execution ...") + logger.info("Experiment execution ...") if exp.base_feature_codes: factors = process_factor_data(exp) factors = factors.sort_index() @@ -275,7 +271,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): target_path = exp.experiment_workspace.workspace_path / "combined_factors_df.parquet" # Save the combined factors to the workspace factors.to_parquet(target_path, engine="pyarrow") - logger.info(f"Factor data processing completed.") + logger.info("Factor data processing completed.") result, stdout = exp.experiment_workspace.execute( qlib_config_name="conf_combined_factors.yaml", run_env=env_to_use, @@ -288,10 +284,10 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): # Handle Qlib Docker backtest failure gracefully if result is None: - factor_name = getattr(exp.hypothesis, 'hypothesis', 'unknown') + factor_name = getattr(exp.hypothesis, "hypothesis", "unknown") logger.warning( f"Qlib Docker backtest returned None for '{factor_name}'. " - f"Attempting direct factor evaluation..." + f"Attempting direct factor evaluation...", ) # Try to compute metrics directly from the factor's result.h5 @@ -303,7 +299,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): else: logger.error( f"Both Qlib Docker backtest and direct evaluation failed for '{factor_name}'. " - f"Skipping this factor and continuing." + f"Skipping this factor and continuing.", ) # Save failed run info for debugging self._save_failed_run(exp, stdout, error_type="result_none") @@ -321,7 +317,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): if validation_result.get("has_issues"): logger.warning( f"Result validation warnings for factor '{getattr(exp.hypothesis, 'hypothesis', 'unknown')}': " - f"{validation_result['warnings']}" + f"{validation_result['warnings']}", ) # Save warning info for debugging self._save_failed_run(exp, stdout, error_type="validation_warnings", validation=validation_result) @@ -372,43 +368,43 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): details = {} factor_name = "unknown" - if hasattr(exp, 'hypothesis') and exp.hypothesis is not None: - factor_name = getattr(exp.hypothesis, 'hypothesis', 'unknown') + if hasattr(exp, "hypothesis") and exp.hypothesis is not None: + factor_name = getattr(exp.hypothesis, "hypothesis", "unknown") if isinstance(result, pd.Series): # Check IC - ic_value = result.get('IC', None) - details['ic_raw'] = ic_value + ic_value = result.get("IC", None) + details["ic_raw"] = ic_value if ic_value is None or (isinstance(ic_value, float) and (ic_value != ic_value)): # NaN check warnings.append("IC is None/NaN — factor has no predictive power") else: try: ic_float = float(ic_value) - details['ic'] = ic_float + details["ic"] = ic_float if abs(ic_float) < 0.001: warnings.append( - f"IC is near zero ({ic_float:.6f}) — factor may not predict returns" + f"IC is near zero ({ic_float:.6f}) — factor may not predict returns", ) except (ValueError, TypeError): warnings.append(f"IC value is not numeric: {ic_value}") # Check positions (1day.pos) - pos_value = result.get('1day.pos', None) - details['positions_raw'] = pos_value + pos_value = result.get("1day.pos", None) + details["positions_raw"] = pos_value if pos_value is not None: try: pos_float = float(pos_value) - details['positions'] = pos_float + details["positions"] = pos_float if pos_float == 0: warnings.append( "1day.pos == 0 — model opened ZERO positions (stayed neutral). " "Possible causes: (1) topk too high for single-asset, " - "(2) signal threshold too restrictive, (3) no valid predictions" + "(2) signal threshold too restrictive, (3) no valid predictions", ) elif pos_float < 10: warnings.append( f"1day.pos = {pos_float:.0f} — very few positions opened. " - f"Check signal threshold and topk settings" + f"Check signal threshold and topk settings", ) except (ValueError, TypeError): pass # pos might be a string @@ -416,24 +412,24 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): # Check if result is essentially empty (all values None or NaN) non_null_count = result.notna().sum() total_count = len(result) - details['non_null_metrics'] = int(non_null_count) - details['total_metrics'] = int(total_count) + details["non_null_metrics"] = int(non_null_count) + details["total_metrics"] = int(total_count) if non_null_count < 3: warnings.append( f"Result has only {non_null_count}/{total_count} non-null metrics — " - f"backtest likely produced empty results" + f"backtest likely produced empty results", ) # Check for key metrics - required_metrics = ['IC', '1day.excess_return_with_cost.shar', '1day.pos'] + required_metrics = ["IC", "1day.excess_return_with_cost.shar", "1day.pos"] for metric_name in required_metrics: val = result.get(metric_name, None) - details[f'has_{metric_name}'] = val is not None + details[f"has_{metric_name}"] = val is not None elif isinstance(result, dict): # Dict-based result validation - ic_value = result.get('IC', result.get('ic', None)) - details['ic_raw'] = ic_value + ic_value = result.get("IC", result.get("ic", None)) + details["ic_raw"] = ic_value if ic_value is None: warnings.append("IC is None — factor has no predictive power") @@ -443,7 +439,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): "details": details, } - def _evaluate_factor_directly(self, exp, stdout: str) -> Optional[pd.Series]: + def _evaluate_factor_directly(self, exp, stdout: str) -> pd.Series | None: """ Evaluate factor directly from its result.h5 file when Qlib Docker fails. @@ -475,7 +471,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): workspace_path = None if exp.sub_workspace_list: for ws in exp.sub_workspace_list: - if ws is not None and hasattr(ws, 'workspace_path'): + if ws is not None and hasattr(ws, "workspace_path"): candidate = ws.workspace_path / "result.h5" if candidate.exists(): workspace_path = ws.workspace_path @@ -579,7 +575,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): logger.info( f"Direct evaluation: IC={ic:.6f}, Sharpe={sharpe:.4f}, " - f"AnnRet={annualized_return:.4f}%, WR={win_rate:.2%}" + f"AnnRet={annualized_return:.4f}%, WR={win_rate:.2%}", ) return result @@ -588,7 +584,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): return None def _save_failed_run(self, exp, stdout: str, error_type: str = "unknown", - validation: Optional[dict] = None) -> None: + validation: dict | None = None) -> None: """ Save failed run information to results/failed_runs.json for debugging. @@ -615,20 +611,20 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): # Get factor name factor_name = "unknown" - if hasattr(exp, 'hypothesis') and exp.hypothesis is not None: - factor_name = getattr(exp.hypothesis, 'hypothesis', 'unknown') + if hasattr(exp, "hypothesis") and exp.hypothesis is not None: + factor_name = getattr(exp.hypothesis, "hypothesis", "unknown") # Build failed run record failed_record = { "timestamp": datetime.now().isoformat(), "factor_name": factor_name, "error_type": error_type, - "stdout": stdout if stdout else "(empty)", + "stdout": stdout or "(empty)", "validation": validation, "experiment_details": { - "base_features": list(getattr(exp, 'base_features', {}).keys()) if hasattr(exp, 'base_features') else [], - "hypothesis": getattr(exp.hypothesis, 'hypothesis', str(getattr(exp, 'hypothesis', 'N/A'))) - if hasattr(exp, 'hypothesis') else "N/A", + "base_features": list(getattr(exp, "base_features", {}).keys()) if hasattr(exp, "base_features") else [], + "hypothesis": getattr(exp.hypothesis, "hypothesis", str(getattr(exp, "hypothesis", "N/A"))) + if hasattr(exp, "hypothesis") else "N/A", }, } @@ -651,11 +647,11 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): failed_file.write_text( json.dumps(existing_records, indent=2, default=str, ensure_ascii=False), - encoding="utf-8" + encoding="utf-8", ) logger.info( f"Failed run saved: {factor_name} (type={error_type}) " - f"→ {failed_file}" + f"→ {failed_file}", ) except Exception as e: @@ -678,23 +674,23 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): containing metric names like 'IC', '1day.excess_return_with_cost.shar', etc. """ try: - import json - import pandas as pd from pathlib import Path + + import pandas as pd from rdagent.components.backtesting import ResultsDatabase # Get factor name: prefer hypothesis, fallback to result Series 'factor_name' key factor_name = "unknown" - if hasattr(exp, 'hypothesis') and exp.hypothesis is not None: - factor_name = getattr(exp.hypothesis, 'hypothesis', 'unknown') - if factor_name == 'unknown' and isinstance(result, pd.Series) and 'factor_name' in result.index: - factor_name = str(result['factor_name']) + if hasattr(exp, "hypothesis") and exp.hypothesis is not None: + factor_name = getattr(exp.hypothesis, "hypothesis", "unknown") + if factor_name == "unknown" and isinstance(result, pd.Series) and "factor_name" in result.index: + factor_name = str(result["factor_name"]) # Check if already rejected by protection - if getattr(exp, 'rejected_by_protection', False): + if getattr(exp, "rejected_by_protection", False): logger.info( f"Factor rejected by protection, skipping DB save: " - f"{getattr(exp, 'protection_reason', 'unknown')}" + f"{getattr(exp, 'protection_reason', 'unknown')}", ) return @@ -710,47 +706,47 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): # Extract metrics from result (pd.Series from qlib_res.csv) metrics = {} if isinstance(result, pd.Series): - metrics['ic'] = self._safe_float(result.get('IC', None)) - metrics['sharpe_ratio'] = self._safe_float( - result.get('1day.excess_return_with_cost.shar', - result.get('1day.excess_return_with_cost.sharpe', None)) + metrics["ic"] = self._safe_float(result.get("IC", None)) + metrics["sharpe_ratio"] = self._safe_float( + result.get("1day.excess_return_with_cost.shar", + result.get("1day.excess_return_with_cost.sharpe", None)), ) - metrics['annualized_return'] = self._safe_float( - result.get('1day.excess_return_with_cost.annualized_return', None) + metrics["annualized_return"] = self._safe_float( + result.get("1day.excess_return_with_cost.annualized_return", None), ) - metrics['max_drawdown'] = self._safe_float( - result.get('1day.excess_return_with_cost.max_drawdown', None) + metrics["max_drawdown"] = self._safe_float( + result.get("1day.excess_return_with_cost.max_drawdown", None), ) - metrics['win_rate'] = self._safe_float(result.get('win_rate', None)) - metrics['information_ratio'] = self._safe_float( - result.get('1day.excess_return_with_cost.information_ratio', None) + metrics["win_rate"] = self._safe_float(result.get("win_rate", None)) + metrics["information_ratio"] = self._safe_float( + result.get("1day.excess_return_with_cost.information_ratio", None), ) - metrics['volatility'] = self._safe_float( - result.get('1day.excess_return_with_cost.std', - result.get('1day.excess_return_with_cost.volatility', None)) + metrics["volatility"] = self._safe_float( + result.get("1day.excess_return_with_cost.std", + result.get("1day.excess_return_with_cost.volatility", None)), ) # Store raw metrics for JSON export - metrics['raw_metrics'] = result.to_dict() + metrics["raw_metrics"] = result.to_dict() elif isinstance(result, dict): - metrics['ic'] = self._safe_float(result.get('IC', result.get('ic', None))) - metrics['sharpe_ratio'] = self._safe_float( - result.get('sharpe', result.get('sharpe_ratio', None)) + metrics["ic"] = self._safe_float(result.get("IC", result.get("ic", None))) + metrics["sharpe_ratio"] = self._safe_float( + result.get("sharpe", result.get("sharpe_ratio", None)), ) - metrics['annualized_return'] = self._safe_float(result.get('annualized_return', None)) - metrics['max_drawdown'] = self._safe_float(result.get('max_drawdown', None)) - metrics['win_rate'] = self._safe_float(result.get('win_rate', None)) - metrics['information_ratio'] = None - metrics['volatility'] = None - metrics['raw_metrics'] = result + metrics["annualized_return"] = self._safe_float(result.get("annualized_return", None)) + metrics["max_drawdown"] = self._safe_float(result.get("max_drawdown", None)) + metrics["win_rate"] = self._safe_float(result.get("win_rate", None)) + metrics["information_ratio"] = None + metrics["volatility"] = None + metrics["raw_metrics"] = result # Result validation before saving (warnings, not blocking) self._log_result_warnings(factor_name, result, metrics) # Only save if we have at least IC or Sharpe - if metrics.get('ic') is None and metrics.get('sharpe_ratio') is None: + if metrics.get("ic") is None and metrics.get("sharpe_ratio") is None: logger.warning( f"No valid IC/Sharpe for factor '{factor_name}', skipping DB save. " - f"IC={metrics.get('ic')}, Sharpe={metrics.get('sharpe_ratio')}" + f"IC={metrics.get('ic')}, Sharpe={metrics.get('sharpe_ratio')}", ) return @@ -773,7 +769,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): run_id = db.add_backtest(factor_name=factor_name[:100], metrics=metrics) logger.info( f"Factor result saved to DB: {factor_name[:60]} " - f"(IC={metrics.get('ic')}, Sharpe={metrics.get('sharpe_ratio')}, run_id={run_id})" + f"(IC={metrics.get('ic')}, Sharpe={metrics.get('sharpe_ratio')}, run_id={run_id})", ) # Extract factor code and description from experiment @@ -784,7 +780,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): factor_name, metrics, run_id, factor_code=factor_code, factor_description=factor_description, - exp=exp + exp=exp, ) # Save factor values as parquet for strategy building @@ -796,7 +792,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): import traceback logger.error( f"Database save failed for factor '{getattr(exp.hypothesis, 'hypothesis', 'unknown')}': {e}\n" - f"Traceback: {traceback.format_exc()}" + f"Traceback: {traceback.format_exc()}", ) def _save_factor_json(self, factor_name: str, metrics: dict, run_id: int, @@ -907,14 +903,14 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): factor_description = match.group(1).strip()[:500] else: # Try comments - lines = factor_code.split('\n') + lines = factor_code.split("\n") desc_lines = [] for line in lines[:20]: stripped = line.strip() - if stripped.startswith('#') and not stripped.startswith('#!'): + if stripped.startswith("#") and not stripped.startswith("#!"): desc_lines.append(stripped[1:].strip()) if desc_lines: - factor_description = ' '.join(desc_lines)[:500] + factor_description = " ".join(desc_lines)[:500] return factor_code, factor_description @@ -926,8 +922,8 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): the complete backtest range (not just the debug 2024 subset). """ import os as _os - import subprocess import shutil + import subprocess import tempfile try: @@ -935,7 +931,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): workspace_path = None if exp.sub_workspace_list: for ws in exp.sub_workspace_list: - if ws is not None and hasattr(ws, 'workspace_path'): + if ws is not None and hasattr(ws, "workspace_path"): fp = ws.workspace_path / "factor.py" if fp.exists(): workspace_path = ws.workspace_path @@ -1023,7 +1019,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): warnings_list = [] # Check IC - ic = metrics.get('ic') + ic = metrics.get("ic") if ic is None: warnings_list.append("IC is None — factor has no predictive power") elif abs(ic) < 0.001: @@ -1031,7 +1027,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): # Check positions (1day.pos) — CRITICAL for EURUSD if isinstance(result, pd.Series): - pos_value = result.get('1day.pos', None) + pos_value = result.get("1day.pos", None) if pos_value is not None: try: pos_float = float(pos_value) @@ -1039,23 +1035,23 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): warnings_list.append( "WARNING: 1day.pos == 0 — ZERO positions opened! " "Model stayed completely neutral. Check Qlib config: " - "ensure topk=1 and market=eurusd for single-asset trading." + "ensure topk=1 and market=eurusd for single-asset trading.", ) elif pos_float < 10: warnings_list.append( f"Low position count: 1day.pos = {pos_float:.0f} — " - f"model traded very rarely" + f"model traded very rarely", ) except (ValueError, TypeError): pass # Check Sharpe - sharpe = metrics.get('sharpe_ratio') + sharpe = metrics.get("sharpe_ratio") if sharpe is not None and abs(sharpe) < 0.1: warnings_list.append(f"Sharpe near zero ({sharpe:.4f}) — no risk-adjusted edge") # Check max drawdown - mdd = metrics.get('max_drawdown') + mdd = metrics.get("max_drawdown") if mdd is not None and mdd < -0.5: warnings_list.append(f"Extreme drawdown: {mdd:.2%} — high risk factor") @@ -1070,7 +1066,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): return None try: f = float(value) - if pd.isna(f) or f == float('inf') or f == float('-inf'): + if pd.isna(f) or f == float("inf") or f == float("-inf"): return None return f except (ValueError, TypeError): @@ -1113,7 +1109,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): if protection_result.should_block: logger.warning( - f"Factor {factor_name} rejected by protection manager: {protection_result.reason}" + f"Factor {factor_name} rejected by protection manager: {protection_result.reason}", ) # Mark factor as rejected by protection exp.rejected_by_protection = True @@ -1138,8 +1134,8 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): from pathlib import Path factor_name = "unknown" - if hasattr(exp, 'hypothesis') and exp.hypothesis is not None: - factor_name = getattr(exp.hypothesis, 'hypothesis', 'unknown') + if hasattr(exp, "hypothesis") and exp.hypothesis is not None: + factor_name = getattr(exp.hypothesis, "hypothesis", "unknown") # Build log entry log_entry = { @@ -1151,42 +1147,42 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): "annualized_return": None, "max_drawdown": None, "win_rate": None, - "rejected_by_protection": getattr(exp, 'rejected_by_protection', False), - "protection_reason": getattr(exp, 'protection_reason', None), + "rejected_by_protection": getattr(exp, "rejected_by_protection", False), + "protection_reason": getattr(exp, "protection_reason", None), } # Extract metrics if available if result is not None: - if hasattr(result, 'get'): # pd.Series or dict - ic_val = result.get('IC', result.get('ic', None)) - log_entry['ic'] = self._safe_float(ic_val) if ic_val is not None else None + if hasattr(result, "get"): # pd.Series or dict + ic_val = result.get("IC", result.get("ic", None)) + log_entry["ic"] = self._safe_float(ic_val) if ic_val is not None else None - sharpe_val = result.get('1day.excess_return_with_cost.shar', - result.get('1day.excess_return_with_cost.sharpe', - result.get('sharpe', None))) - log_entry['sharpe'] = self._safe_float(sharpe_val) if sharpe_val is not None else None + sharpe_val = result.get("1day.excess_return_with_cost.shar", + result.get("1day.excess_return_with_cost.sharpe", + result.get("sharpe", None))) + log_entry["sharpe"] = self._safe_float(sharpe_val) if sharpe_val is not None else None - ann_ret = result.get('1day.excess_return_with_cost.annualized_return', - result.get('annualized_return', None)) - log_entry['annualized_return'] = self._safe_float(ann_ret) if ann_ret is not None else None + ann_ret = result.get("1day.excess_return_with_cost.annualized_return", + result.get("annualized_return", None)) + log_entry["annualized_return"] = self._safe_float(ann_ret) if ann_ret is not None else None - mdd = result.get('1day.excess_return_with_cost.max_drawdown', - result.get('max_drawdown', None)) - log_entry['max_drawdown'] = self._safe_float(mdd) if mdd is not None else None + mdd = result.get("1day.excess_return_with_cost.max_drawdown", + result.get("max_drawdown", None)) + log_entry["max_drawdown"] = self._safe_float(mdd) if mdd is not None else None - wr = result.get('win_rate', None) - log_entry['win_rate'] = self._safe_float(wr) if wr is not None else None + wr = result.get("win_rate", None) + log_entry["win_rate"] = self._safe_float(wr) if wr is not None else None # Determine status - if log_entry['ic'] is not None or log_entry['sharpe'] is not None: - log_entry['status'] = "success" - elif getattr(exp, 'rejected_by_protection', False): - log_entry['status'] = "rejected_protection" + if log_entry["ic"] is not None or log_entry["sharpe"] is not None: + log_entry["status"] = "success" + elif getattr(exp, "rejected_by_protection", False): + log_entry["status"] = "rejected_protection" else: - log_entry['status'] = "no_valid_metrics" + log_entry["status"] = "no_valid_metrics" else: - log_entry['status'] = "execution_failed" - log_entry['reason'] = "Result was None" + log_entry["status"] = "execution_failed" + log_entry["reason"] = "Result was None" # Write to results/logs/ try: @@ -1209,7 +1205,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): logger.info( f"Run log written for '{factor_name[:50]}': " - f"status={log_entry['status']}, IC={log_entry['ic']}, Sharpe={log_entry['sharpe']}" + f"status={log_entry['status']}, IC={log_entry['ic']}, Sharpe={log_entry['sharpe']}", ) except Exception as e: logger.error(f"Failed to write run log: {e}") diff --git a/rdagent/utils/env.py b/rdagent/utils/env.py index 67c73abf..92650bcd 100644 --- a/rdagent/utils/env.py +++ b/rdagent/utils/env.py @@ -436,13 +436,10 @@ class Env(Generic[ASpecificEnvConf]): else: timeout_cmd = f"timeout --kill-after=10 {self.conf.running_timeout_period} {entry}" entry_add_timeout = ( - f"/bin/sh -c '" # start of the sh command - + f"{timeout_cmd}; entry_exit_code=$?; " + "/bin/sh -c '" # start of the sh command + + timeout_cmd.replace("'", "'\\''") + "; entry_exit_code=$?; " + ( f"{_get_chmod_cmd(self.conf.mount_path)}; " - # We don't have to change the permission of the cache and input folder to remove it - # + f"if [ -d {self.conf.mount_path}/cache ]; then chmod 777 {self.conf.mount_path}/cache; fi; " + - # f"if [ -d {self.conf.mount_path}/input ]; then chmod 777 {self.conf.mount_path}/input; fi; " if isinstance(self.conf, DockerConf) else "" ) @@ -1524,8 +1521,8 @@ class DockerEnv(Env[DockerConf]): class QTDockerEnv(DockerEnv): """Qlib Torch Docker""" - def __init__(self, conf: DockerConf = QlibDockerConf()): - super().__init__(conf) + def __init__(self, conf: DockerConf | None = None): + super().__init__(conf if conf is not None else QlibDockerConf()) def prepare(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] """ @@ -1544,15 +1541,15 @@ class QTDockerEnv(DockerEnv): class KGDockerEnv(DockerEnv): """Kaggle Competition Docker""" - def __init__(self, competition: str | None = None, conf: DockerConf = KGDockerConf()): - super().__init__(conf) + def __init__(self, competition: str | None = None, conf: DockerConf | None = None): + super().__init__(conf if conf is not None else KGDockerConf()) class MLEBDockerEnv(DockerEnv): """MLEBench Docker""" - def __init__(self, conf: DockerConf = MLEBDockerConf()): - super().__init__(conf) + def __init__(self, conf: DockerConf | None = None): + super().__init__(conf if conf is not None else MLEBDockerConf()) class FTDockerEnv(DockerEnv): @@ -1568,8 +1565,8 @@ class FTDockerEnv(DockerEnv): export FT_DOCKER_save_logs_to_file=false # disable log file """ - def __init__(self, conf: DockerConf = FTDockerConf()): - super().__init__(conf) + def __init__(self, conf: DockerConf | None = None): + super().__init__(conf if conf is not None else FTDockerConf()) class BenchmarkDockerEnv(DockerEnv): @@ -1586,5 +1583,5 @@ class BenchmarkDockerEnv(DockerEnv): export BENCHMARK_DOCKER_terminal_tail_lines=100 # show last 100 lines """ - def __init__(self, conf: DockerConf = BenchmarkDockerConf()): - super().__init__(conf) + def __init__(self, conf: DockerConf | None = None): + super().__init__(conf if conf is not None else BenchmarkDockerConf())