From f01960ab5511491ff0b9332be78b5335a828859c Mon Sep 17 00:00:00 2001 From: TPTBusiness Date: Thu, 2 Apr 2026 22:40:46 +0200 Subject: [PATCH] feat: Add model loader system (same as prompts) New structure: - models/standard/*.py: Default models (XGBoost, LightGBM, RandomForest) - models/local/*.py: Your improved models (NOT in Git!) - models/README.md: Documentation - rdagent/components/model_loader.py: Model loader with priority Features: - Loader checks models/local/ first (your better models) - Falls back to models/standard/ if no local version - Supports versioned models (model_v2.py, model_v1.py) - Lists available models - Test function included .gitignore updated: - models/local/ excluded (your proprietary models) - *.local.py excluded - *_private.py excluded Usage: from rdagent.components.model_loader import load_model model = load_model('xgboost_factor') # Auto-loads your better version! Standard models included: - xgboost_factor.py: XGBoost for tabular data - lightgbm_factor.py: LightGBM (faster than XGBoost) --- .gitignore | 5 + models/README.md | 239 +++++++++++++++++++++++++++++ models/standard/lightgbm_factor.py | 98 ++++++++++++ models/standard/xgboost_factor.py | 90 +++++++++++ rdagent/app/finetune/llm/ui/app.py | 51 +++--- rdagent/components/loader.py | 34 +++- rdagent/components/model_loader.py | 194 +++++++++++++++++++++++ 7 files changed, 680 insertions(+), 31 deletions(-) create mode 100644 models/README.md create mode 100644 models/standard/lightgbm_factor.py create mode 100644 models/standard/xgboost_factor.py create mode 100644 rdagent/components/model_loader.py diff --git a/.gitignore b/.gitignore index 552b70e7..81845674 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,11 @@ prompts/local/ *.local.yaml *_private.yaml +# Private models (your improved versions) +models/local/ +*.local.py +*_private.py + # Test credentials .env.test *.test.env diff --git a/models/README.md b/models/README.md new file mode 100644 index 00000000..1c3e4cc4 --- /dev/null +++ b/models/README.md @@ -0,0 +1,239 @@ +# Predix Models + +This directory contains all ML model definitions for Predix trading factors. + +--- + +## 📁 Directory Structure + +``` +models/ +├── standard/ # Default models (committed to Git) +│ ├── xgboost_factor.py # XGBoost for tabular data +│ ├── lightgbm_factor.py # LightGBM (faster than XGBoost) +│ └── randomforest_factor.py # Baseline model +│ +├── local/ # YOUR IMPROVED MODELS (not in Git!) +│ ├── transformer_factor.py # Your Transformer +│ ├── tcn_factor.py # Your TCN +│ ├── patchtst_factor.py # Your PatchTST +│ ├── cnn_lstm_hybrid.py # Your Hybrid model +│ └── optimized_xgboost.py # Your optimized XGBoost +│ +└── README.md # This file +``` + +--- + +## 🎯 How It Works + +**Model Loading Priority:** + +1. **`models/local/*.py`** ← Your improved models (loaded first!) +2. **`models/standard/*.py`** ← Default models (fallback) + +**Example:** +```python +from rdagent.components.model_loader import load_model + +# Load XGBoost model +# If models/local/xgboost_factor*.py exists → loads that +# Otherwise → loads from models/standard/ +model_factory = load_model("xgboost_factor") + +# Create model instance +model = model_factory(max_depth=8, learning_rate=0.1) + +# Train +model.fit(X_train, y_train) + +# Predict +predictions = model.predict(X_test) +``` + +--- + +## 📝 Available Standard Models + +| Model | File | Use Case | +|-------|------|----------| +| **XGBoost** | `xgboost_factor.py` | Tabular factors, fast training | +| **LightGBM** | `lightgbm_factor.py` | Large datasets, faster than XGBoost | +| **RandomForest** | `randomforest_factor.py` | Baseline, robust | + +--- + +## 🚀 Creating Your Improved Models + +### Step 1: Create Local Model File + +```bash +# Create local directory (if not exists) +mkdir -p models/local + +# Copy standard model as template +cp models/standard/xgboost_factor.py models/local/optimized_xgboost.py +``` + +### Step 2: Improve Your Model + +```python +# models/local/optimized_xgboost.py + +class XGBoostFactorModel: + """Your optimized version with better hyperparameters.""" + + def __init__(self, **params): + self.params = { + 'objective': 'reg:squarederror', + 'max_depth': 8, # Deeper trees + 'learning_rate': 0.03, # Slower learning + 'n_estimators': 1000, # More estimators + 'subsample': 0.9, # Less dropout + 'colsample_bytree': 0.9, + 'random_state': 42, + # Your custom params + 'gamma': 0.1, # Regularization + 'min_child_weight': 3, + **params + } + # ... rest of implementation +``` + +### Step 3: Use in Trading + +Your improved models are automatically used when running: + +```python +from rdagent.components.model_loader import load_model + +# Auto-loads your optimized version! +model_factory = load_model("xgboost_factor") +``` + +--- + +## 🔐 Security + +**What to keep in `models/local/`:** + +✅ Your proprietary model architectures +✅ Optimized hyperparameters +✅ Custom feature engineering +✅ Ensemble methods +✅ Trade secrets & alpha-generating logic + +**What NOT to commit to Git:** + +❌ Anything in `models/local/` (already in .gitignore) +❌ Files with `.local.py` suffix +❌ Files with `_private.py` suffix + +--- + +## 📊 Best Practices + +### 1. Version Your Models + +```python +# Good naming: +models/local/ +├── xgboost_v2.py # Version 2 +├── xgboost_v3_optimized.py # Version 3 optimized +└── lightgbm_lstm_hybrid_v1.py # Hybrid v1 +``` + +### 2. Document Changes + +```python +# models/local/optimized_xgboost_v2.py +""" +XGBoost Factor Model v2.0 + +Changes from v1: +- Increased max_depth from 6 to 8 +- Added gamma regularization +- Increased n_estimators from 500 to 1000 +- Target: +2% ARR, +0.2 Sharpe + +Author: Your Name +Date: 2026-04-02 +""" +``` + +### 3. Test Performance + +```python +# Compare model versions +from rdagent.components.model_loader import load_model + +# Load standard +std_model = load_model("xgboost_factor", local_only=False) + +# Load local (if exists) +local_model = load_model("xgboost_factor", local_only=True) + +# Backtest both and compare +# ... +``` + +--- + +## 🔧 Advanced Usage + +### Load All Models + +```python +from rdagent.components.model_loader import list_available_models + +all_models = list_available_models() +print(f"Standard: {all_models['standard']}") +print(f"Local: {all_models['local']}") +``` + +### Force Local Model + +```python +# Raise error if local model not found +model = load_model("transformer_factor", local_only=True) +``` + +### Custom Model Path + +```python +from rdagent.components.model_loader import load_module_from_path +from pathlib import Path + +# Load from custom location +module = load_module_from_path( + Path("/path/to/my/custom_model.py"), + "custom_model" +) +``` + +--- + +## 📈 Model Selection Guide + +| Scenario | Recommended Model | Why | +|----------|------------------|-----| +| **Tabular Factors** | XGBoost / LightGBM | Fast, interpretable | +| **Large Dataset** | LightGBM | Lower memory, faster | +| **Baseline** | RandomForest | Robust, no tuning needed | +| **Time-Series Patterns** | LSTM / GRU (local) | Sequential dependencies | +| **Multi-Scale** | TCN (local) | Different time horizons | +| **Long-Range** | Transformer (local) | Attention mechanism | +| **Best Performance** | Ensemble (local) | Combine multiple models | + +--- + +## 🎯 Next Steps + +1. **Review standard models:** `cat models/standard/*.py` +2. **Create your improved version:** `mkdir -p models/local` +3. **Test:** `python rdagent/components/model_loader.py` +4. **Run trading:** `rdagent fin_quant` + +--- + +**Your improved models in `models/local/` are your competitive edge! 🚀** diff --git a/models/standard/lightgbm_factor.py b/models/standard/lightgbm_factor.py new file mode 100644 index 00000000..7163d1c3 --- /dev/null +++ b/models/standard/lightgbm_factor.py @@ -0,0 +1,98 @@ +""" +LightGBM Factor Model - Standard Version + +Usage: + from rdagent.components.model_loader import load_model + model = load_model("lightgbm_factor") +""" + +import lightgbm as lgb +import numpy as np +import pandas as pd +from pathlib import Path + + +class LightGBMFactorModel: + """ + LightGBM-based factor model for EUR/USD trading. + + Features: + - Faster than XGBoost + - Lower memory usage + - Good for large datasets + """ + + def __init__(self, **params): + self.params = { + 'objective': 'regression', + 'metric': 'mse', + 'num_leaves': 31, + 'learning_rate': 0.05, + 'feature_fraction': 0.8, + 'bagging_fraction': 0.8, + 'bagging_freq': 5, + 'verbose': -1, + 'random_state': 42, + **params + } + self.model = None + self.feature_names = None + + def fit(self, X, y, feature_names=None, **fit_params): + """Train the model.""" + self.feature_names = feature_names + + # Create LightGBM datasets + train_data = lgb.Dataset(X, label=y, feature_name=feature_names if feature_names else 'auto') + + self.model = lgb.train( + self.params, + train_data, + num_boost_round=500, + **fit_params + ) + + return self + + def predict(self, X): + """Generate predictions.""" + if self.model is None: + raise ValueError("Model not trained. Call fit() first.") + + return self.model.predict(X) + + def get_feature_importance(self, top_n=10, importance_type='gain'): + """Get top N most important features.""" + if self.model is None: + raise ValueError("Model not trained.") + + importance = self.model.feature_importance(importance_type=importance_type) + if self.feature_names is not None: + indices = np.argsort(importance)[::-1][:top_n] + return [(self.feature_names[i], importance[i]) for i in indices] + return importance + + def save(self, path: str): + """Save model to file.""" + Path(path).parent.mkdir(parents=True, exist_ok=True) + self.model.save_model(path) + print(f"✓ Model saved to {path}") + + def load(self, path: str): + """Load model from file.""" + self.model = lgb.Booster(model_file=path) + print(f"✓ Model loaded from {path}") + return self + + +# Convenience function +def create_lightgbm_factor_model(**params): + """Create LightGBM factor model.""" + return LightGBMFactorModel(**params) + + +if __name__ == "__main__": + # Test + print("=== LightGBM Factor Model Test ===") + model = create_lightgbm_factor_model() + print(f"✓ Model created with params: {model.params}") diff --git a/models/standard/xgboost_factor.py b/models/standard/xgboost_factor.py new file mode 100644 index 00000000..2ce0b8ab --- /dev/null +++ b/models/standard/xgboost_factor.py @@ -0,0 +1,90 @@ +""" +XGBoost Factor Model - Standard Version + +Usage: + from rdagent.components.model_loader import load_model + model = load_model("xgboost_factor") +""" + +import xgboost as xgb +import numpy as np +import pandas as pd +from pathlib import Path + + +class XGBoostFactorModel: + """ + XGBoost-based factor model for EUR/USD trading. + + Features: + - Handles tabular data efficiently + - Built-in feature importance + - Fast training and inference + """ + + def __init__(self, **params): + self.params = { + 'objective': 'reg:squarederror', + 'max_depth': 6, + 'learning_rate': 0.05, + 'n_estimators': 500, + 'subsample': 0.8, + 'colsample_bytree': 0.8, + 'random_state': 42, + **params + } + self.model = None + self.feature_names = None + + def fit(self, X, y, feature_names=None, **fit_params): + """Train the model.""" + self.feature_names = feature_names + + self.model = xgb.XGBRegressor(**self.params) + self.model.fit(X, y, **fit_params) + + return self + + def predict(self, X): + """Generate predictions.""" + if self.model is None: + raise ValueError("Model not trained. Call fit() first.") + + return self.model.predict(X) + + def get_feature_importance(self, top_n=10): + """Get top N most important features.""" + if self.model is None: + raise ValueError("Model not trained.") + + importance = self.model.feature_importances_ + if self.feature_names is not None: + indices = np.argsort(importance)[::-1][:top_n] + return [(self.feature_names[i], importance[i]) for i in indices] + return importance + + def save(self, path: str): + """Save model to file.""" + Path(path).parent.mkdir(parents=True, exist_ok=True) + self.model.save_model(path) + print(f"✓ Model saved to {path}") + + def load(self, path: str): + """Load model from file.""" + self.model = xgb.XGBRegressor() + self.model.load_model(path) + print(f"✓ Model loaded from {path}") + return self + + +# Convenience function +def create_xgboost_factor_model(**params): + """Create XGBoost factor model.""" + return XGBoostFactorModel(**params) + + +if __name__ == "__main__": + # Test + print("=== XGBoost Factor Model Test ===") + model = create_xgboost_factor_model() + print(f"✓ Model created with params: {model.params}") diff --git a/rdagent/app/finetune/llm/ui/app.py b/rdagent/app/finetune/llm/ui/app.py index 6f9a74d1..0a05af27 100644 --- a/rdagent/app/finetune/llm/ui/app.py +++ b/rdagent/app/finetune/llm/ui/app.py @@ -38,24 +38,18 @@ def get_job_options(base_path: Path) -> list[str]: has_root_tasks = False job_dirs = [] - # Security fix: Validate base_path to prevent path traversal - # Resolve to absolute path and ensure it's within allowed boundaries + # Security: Validate base_path to prevent path traversal + # Resolve to absolute path and ensure it's within the current working directory. try: - base_path_resolved = base_path.resolve(strict=False) - cwd_resolved = Path.cwd().resolve() - - # Ensure base_path is within or relative to current working directory - # This prevents accessing arbitrary filesystem locations - try: - base_path_resolved.relative_to(cwd_resolved) - except ValueError: - # Path is outside CWD, check if it's a safe relative path - if base_path_resolved.is_relative_to(cwd_resolved): - pass # OK - else: - # Path is completely outside project, reject it - st.error(f"Invalid log base path: Must be within project directory") - return options + safe_root = Path.cwd().resolve() + base_path_resolved = base_path.expanduser().resolve(strict=False) + + # Ensure base_path_resolved is within safe_root; raises ValueError if not. + base_path_resolved.relative_to(safe_root) + except ValueError: + # Path is outside the allowed root, reject it. + st.error("Invalid log base path: Must be within project directory") + return options except (OSError, RuntimeError) as e: st.error(f"Invalid path: {e}") return options @@ -175,22 +169,25 @@ def main(): # ========== Main Content ========== if view_mode == "Job Summary": st.title("📊 FT Job Summary") - - # Security fix: Validate job_folder to prevent path traversal + + # Security: Validate job_folder to prevent path traversal # Only allow paths within the base_path directory try: - job_path = Path(job_folder).resolve() - base_path_resolved = Path(base_path).resolve() - - # Ensure job_path is within base_path (prevent path traversal) - job_path.relative_to(base_path_resolved) - + safe_root = Path(base_path).resolve() + job_path = Path(job_folder).expanduser().resolve(strict=False) + + # Ensure job_path is within safe_root (prevent path traversal) + job_path.relative_to(safe_root) + if job_path.exists(): render_job_summary(job_path, is_root=is_root_job) else: st.warning(f"Job folder not found: {job_folder}") - except (ValueError, RuntimeError) as e: - st.error(f"Invalid job folder path: {e}") + except ValueError: + st.error("Invalid job folder path: Must be within base directory") + st.info("Please select a valid job from the sidebar.") + except (OSError, RuntimeError) as e: + st.error(f"Invalid path: {e}") st.info("Please select a valid job from the sidebar.") return diff --git a/rdagent/components/loader.py b/rdagent/components/loader.py index a13c963a..aa2c1984 100644 --- a/rdagent/components/loader.py +++ b/rdagent/components/loader.py @@ -29,11 +29,25 @@ STANDARD_PROMPTS_FILE = PROMPTS_DIR / "standard_prompts.yaml" def get_local_prompt_path(name: str) -> Optional[Path]: - """Find local prompt file by name.""" + """Find local prompt file by name. + + Priority: + 1. {name}_v2.yaml (latest version) + 2. {name}_v1.yaml + 3. {name}.yaml + """ if not LOCAL_PROMPTS_DIR.exists(): return None - # Try different file extensions + # Try versioned files first (v2, v1, etc.) + for version in ["v2", "v1"]: + for ext in ["yaml", "yml"]: + path = LOCAL_PROMPTS_DIR / f"{name}_{version}.{ext}" + if path.exists(): + print(f" (found versioned: {name}_{version}.{ext})") + return path + + # Try exact name for ext in ["yaml", "yml"]: path = LOCAL_PROMPTS_DIR / f"{name}.{ext}" if path.exists(): @@ -187,7 +201,19 @@ if __name__ == "__main__": try: prompt = load_prompt("factor_discovery") print(f"✓ Loaded factor_discovery prompt") - print(f" System: {len(prompt.get('system', ''))} chars") - print(f" User: {len(prompt.get('user', ''))} chars") + + # Handle nested dict structure (local prompts) + if isinstance(prompt, dict): + if 'factor_discovery' in prompt: + # Local prompt structure + fd = prompt['factor_discovery'] + print(f" System: {len(fd.get('system', ''))} chars") + print(f" User: {len(fd.get('user', ''))} chars") + else: + # Standard prompt structure + print(f" System: {len(prompt.get('system', ''))} chars") + print(f" User: {len(prompt.get('user', ''))} chars") + else: + print(f" Content: {len(str(prompt))} chars") except FileNotFoundError as e: print(f"✗ Error: {e}") diff --git a/rdagent/components/model_loader.py b/rdagent/components/model_loader.py new file mode 100644 index 00000000..ec58fb22 --- /dev/null +++ b/rdagent/components/model_loader.py @@ -0,0 +1,194 @@ +""" +Predix Model Loader + +Loads models from: +1. models/local/*.py (your improved models - not in Git) +2. models/standard/*.py (default models - in Git) + +Usage: + from rdagent.components.model_loader import load_model + + # Load XGBoost model + model = load_model("xgboost_factor") + + # Load your improved version (if exists in models/local/) + model = load_model("transformer_factor") # Auto-loads from local if exists +""" + +import os +import sys +import importlib.util +from pathlib import Path +from typing import Optional, Any + + +# Base paths +BASE_DIR = Path(__file__).parent.parent.parent # Predix/ +MODELS_DIR = BASE_DIR / "models" +LOCAL_MODELS_DIR = MODELS_DIR / "local" +STANDARD_MODELS_DIR = MODELS_DIR / "standard" + + +def get_local_model_path(name: str) -> Optional[Path]: + """Find local model file by name. + + Priority: + 1. {name}_v2.py (latest version) + 2. {name}_v1.py + 3. {name}.py + """ + if not LOCAL_MODELS_DIR.exists(): + return None + + # Try versioned files first (v2, v1, etc.) + for version in ["v2", "v1"]: + path = LOCAL_MODELS_DIR / f"{name}_{version}.py" + if path.exists(): + print(f" (found versioned: {name}_{version}.py)") + return path + + # Try exact name + path = LOCAL_MODELS_DIR / f"{name}.py" + if path.exists(): + return path + + return None + + +def get_standard_model_path(name: str) -> Optional[Path]: + """Find standard model file by name.""" + if not STANDARD_MODELS_DIR.exists(): + return None + + path = STANDARD_MODELS_DIR / f"{name}.py" + if path.exists(): + return path + + return None + + +def load_module_from_path(path: Path, module_name: str) -> Any: + """Load Python module from file path.""" + spec = importlib.util.spec_from_file_location(module_name, path) + if spec is None or spec.loader is None: + raise ImportError(f"Cannot load module from {path}") + + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + + return module + + +def load_model(name: str, local_only: bool = False, fallback_to_standard: bool = True): + """ + Load a model by name. + + Priority: + 1. models/local/{name}.py (if exists) + 2. models/standard/{name}.py (if fallback_to_standard=True) + + Args: + name: Model name (e.g., "xgboost_factor", "transformer_factor") + local_only: Only load from local/, raise error if not found + fallback_to_standard: If True, fall back to standard models + + Returns: + Model class or instance + + Raises: + FileNotFoundError: If model not found + ImportError: If model cannot be loaded + """ + # Try local models first + local_path = get_local_model_path(name) + + if local_path: + print(f"✓ Loading model '{name}' from local: {local_path}") + module = load_module_from_path(local_path, f"local_{name}") + + # Try to find create_* or Model class + for attr_name in dir(module): + if attr_name.startswith('create_') and name.replace('_', '') in attr_name.replace('create_', ''): + return getattr(module, attr_name) + if attr_name.endswith('Model') and name.replace('_', '') in attr_name.lower(): + return getattr(module, attr_name) + + # Return module if no specific class found + return module + + # Local not found + if local_only: + raise FileNotFoundError(f"Local model '{name}' not found in {LOCAL_MODELS_DIR}") + + # Try standard models + if not fallback_to_standard: + raise FileNotFoundError(f"Model '{name}' not found") + + standard_path = get_standard_model_path(name) + if not standard_path: + raise FileNotFoundError(f"Model '{name}' not found in standard or local directories") + + print(f"✓ Loading model '{name}' from standard: {standard_path}") + module = load_module_from_path(standard_path, f"standard_{name}") + + # Try to find create_* or Model class + for attr_name in dir(module): + if attr_name.startswith('create_') and name.replace('_', '') in attr_name.replace('create_', ''): + return getattr(module, attr_name) + if attr_name.endswith('Model') and name.replace('_', '') in attr_name.lower(): + return getattr(module, attr_name) + + return module + + +def list_available_models() -> dict: + """List all available models.""" + result = {"standard": [], "local": []} + + # Standard models + if STANDARD_MODELS_DIR.exists(): + result["standard"] = [p.stem for p in STANDARD_MODELS_DIR.glob("*.py") if not p.name.startswith('_')] + + # Local models + if LOCAL_MODELS_DIR.exists(): + result["local"] = [p.stem for p in LOCAL_MODELS_DIR.glob("*.py") if not p.name.startswith('_')] + + return result + + +# Convenience functions for specific models +def get_xgboost_model(**params): + """Get XGBoost model.""" + return load_model("xgboost_factor")(**params) + + +def get_lightgbm_model(**params): + """Get LightGBM model.""" + return load_model("lightgbm_factor")(**params) + + +def get_randomforest_model(**params): + """Get RandomForest model.""" + return load_model("randomforest_factor")(**params) + + +# Test function +if __name__ == "__main__": + print("=== Available Models ===") + available = list_available_models() + print(f"Standard: {available['standard']}") + print(f"Local: {available['local']}") + + print("\n=== Testing Model Load ===") + try: + # Test XGBoost + xgb_factory = load_model("xgboost_factor") + print(f"✓ Loaded xgboost_factor") + + # Test LightGBM + lgb_factory = load_model("lightgbm_factor") + print(f"✓ Loaded lightgbm_factor") + + except Exception as e: + print(f"✗ Error: {e}")