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)
This commit is contained in:
TPTBusiness
2026-04-02 22:40:46 +02:00
parent 59e5aeb9d0
commit f01960ab55
7 changed files with 680 additions and 31 deletions
+5
View File
@@ -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
+239
View File
@@ -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! 🚀**
+98
View File
@@ -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}")
+90
View File
@@ -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}")
+24 -27
View File
@@ -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
+30 -4
View File
@@ -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}")
+194
View File
@@ -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}")