feat: Full system integration - RL + Protections + Backtesting + CLI

Connect all Predix components into unified trading system:

INTEGRATION (ALL 295 TESTS PASS):
- RL Trading connected with Protection Manager
- RL Trading connected with Backtesting Engine
- CLI command 'rdagent rl_trading' added (train/backtest/live modes)
- Graceful fallback for users without stable-baselines3

OPEN SOURCE COMPATIBILITY:
- System works WITHOUT stable-baselines3 (momentum fallback)
- System works WITHOUT local models/prompts (uses standard)
- Clear warning messages when optional deps missing
- GitHub users get FULLY WORKING system

CLOSED SOURCE PROTECTION:
- models/local/, prompts/local/, .env stay local only
- .gitignore properly configured
- Our alpha (best models/prompts) remains private

DOCUMENTATION:
- QWEN.md: Open/closed source strategy
- QWEN.md: Development guidelines for AI assistant
- QWEN.md: Open source compatibility principle
- README.md: RL Trading CLI commands and examples
- requirements/rl.txt: Optional RL dependencies

Modified files:
- rdagent/app/cli.py: Added rl_trading command
- rdagent/components/backtesting/backtest_engine.py: RL backtest support
- rdagent/components/coder/rl/costeer.py: Protection Manager integration
- rdagent/components/coder/rl/__init__.py: Conditional imports + fallback
- rdagent/components/coder/rl/fallback.py: NEW - Simple momentum fallback
- requirements.txt: Optional RL deps commented
- requirements/rl.txt: NEW - Full RL dependencies
- test/integration/test_all_features.py: 7 new integration tests
- QWEN.md: Open source strategy + development guidelines
- README.md: RL Trading documentation

295 tests pass: 67 integration + 89 RL + 139 backtesting
This commit is contained in:
TPTBusiness
2026-04-03 13:53:32 +02:00
parent 8457aba0e5
commit 2136741eaa
10 changed files with 927 additions and 30 deletions
+52 -8
View File
@@ -1,15 +1,31 @@
"""RL Trading Agent components for Predix.
This package provides:
- RLCoSTEER: LLM-based code generation for RL training pipelines
- RLCosteer: RL-based trading controller using trained models
- TradingEnv: Gym-compatible trading environment
- RLTradingAgent: Stable Baselines3 wrapper for PPO, A2C, SAC
- Technical indicators: RSI, MACD, Bollinger Bands, CCI, ATR
This package provides reinforcement learning trading capabilities.
Works with or without stable-baselines3 (graceful fallback).
OPEN SOURCE: Full RL system works for all GitHub users.
- With stable-baselines3: Full PPO/A2C/SAC training
- Without stable-baselines3: Simple momentum fallback
CLOSED SOURCE: Your trained models in models/local/
"""
from rdagent.components.coder.rl.agent import RLTradingAgent
from rdagent.components.coder.rl.costeer import RLCoSTEER, RLCosteer
# Try to import stable-baselines3
try:
import stable_baselines3 # noqa: F401
HAS_STABLE_BASELINES3 = True
except ImportError:
HAS_STABLE_BASELINES3 = False
import warnings
warnings.warn(
"stable-baselines3 not installed. RL trading will use simple momentum fallback. "
"Install with: pip install stable-baselines3[extra]",
UserWarning,
)
# Always import core components (work regardless of stable-baselines3)
from rdagent.components.coder.rl.env import TradingEnv
from rdagent.components.coder.rl.indicators import (
calculate_atr,
@@ -20,7 +36,33 @@ from rdagent.components.coder.rl.indicators import (
prepare_features,
)
# Import RL-specific components only if available
if HAS_STABLE_BASELINES3:
from rdagent.components.coder.rl.agent import RLTradingAgent
from rdagent.components.coder.rl.costeer import RLCoSTEER, RLCosteer
else:
# Use fallback implementations
from rdagent.components.coder.rl.fallback import SimpleRLFallback as RLTradingAgent
from rdagent.components.coder.rl.costeer import RLCosteer
# Create RLCoSTEER stub for when stable-baselines3 is not available
class RLCoSTEER: # type: ignore[no-redef]
"""
Stub RLCoSTEER when stable-baselines3 is not available.
This class exists only for import compatibility.
Use RLCosteer with SimpleRLFallback instead.
"""
def __init__(self, *args, **kwargs):
raise NotImplementedError(
"RLCoSTEER requires stable-baselines3. "
"Install with: pip install stable-baselines3[extra]"
)
__all__ = [
"HAS_STABLE_BASELINES3",
"RLCoSTEER",
"RLCosteer",
"RLTradingAgent",
@@ -32,3 +74,5 @@ __all__ = [
"calculate_rsi",
"prepare_features",
]
__version__ = "1.0.0"
+80 -9
View File
@@ -158,7 +158,7 @@ class RLCoSTEER(CoSTEER):
class RLCosteer:
"""
RL-based trading controller.
RL-based trading controller with protection manager integration.
Takes market data, technical indicators, and portfolio state,
then uses a trained RL model to decide position sizing.
@@ -175,6 +175,8 @@ class RLCosteer:
Maximum position size (0 to 1)
risk_limit : float
Maximum drawdown before forcing position close
enable_protections : bool
Enable trading protection manager (default: True)
Examples
--------
@@ -190,12 +192,14 @@ class RLCosteer:
window_size: int = 60,
max_position: float = 1.0,
risk_limit: float = 0.15,
enable_protections: bool = True,
) -> None:
self.model_path = model_path
self.algorithm = algorithm.upper()
self.window_size = window_size
self.max_position = max_position
self.risk_limit = risk_limit
self.enable_protections = enable_protections
# State
self.is_active = False
@@ -203,12 +207,32 @@ class RLCosteer:
self.current_position: float = 0.0
self.peak_equity: float = 0.0
self.trade_history: List[Dict[str, Any]] = []
self.equity_history: List[float] = []
# Protection Manager
self.protection_manager: Optional[Any] = None
if enable_protections:
try:
from rdagent.components.backtesting.protections.protection_manager import (
ProtectionManager,
)
self.protection_manager = ProtectionManager()
self.protection_manager.create_default_protections()
except ImportError:
import warnings
warnings.warn(
"Protection manager not available. Trading protections disabled."
)
self.protection_manager = None
# Market data (set during initialize)
self.prices: np.ndarray = np.array([])
self.indicators: Optional[np.ndarray] = None
self.initial_equity: float = 0.0
self.current_step: int = 0
self.timestamps_history: List[datetime] = []
# Load model if path provided
if model_path is not None and model_path.exists():
@@ -243,9 +267,11 @@ class RLCosteer:
current_equity: float,
cash: float,
position: float,
returns_history: Optional[List[float]] = None,
timestamps: Optional[List[datetime]] = None,
) -> float:
"""
Get trading action from RL model.
Get trading action from RL model with protection checks.
Parameters
----------
@@ -255,21 +281,47 @@ class RLCosteer:
Available cash
position : float
Current position size
returns_history : list, optional
Historical returns for protection checks
timestamps : list, optional
Historical timestamps for protection checks
Returns
-------
float
Target position (-1 to 1)
"""
if not self.is_active or self.model is None:
return 0.0 # Hold if not active
# Check protections first (if enabled and available)
if self.protection_manager and returns_history and len(returns_history) > 0:
peak_equity = max(self.equity_history + [current_equity]) if self.equity_history else current_equity
# Build observation
protection_result = self.protection_manager.check_all(
returns=returns_history,
timestamps=timestamps or self.timestamps_history[-len(returns_history):] if timestamps is None else [],
current_equity=current_equity,
peak_equity=peak_equity,
)
if protection_result.should_block:
# Protection triggered - force close position
return 0.0
# If not active or no model, hold
if not self.is_active or self.model is None:
return 0.0
# Build observation for RL model
observation = self._build_observation(current_equity, cash, position)
# Get action from model
prediction = self.model.predict(observation)
target_position = float(np.asarray(prediction[0]).flatten()[0])
try:
prediction = self.model.predict(observation)
target_position = float(np.asarray(prediction[0]).flatten()[0])
except Exception as e:
import warnings
warnings.warn(f"RL model prediction failed: {e}. Returning hold.")
return 0.0
# Apply risk limits
drawdown = (self.peak_equity - current_equity) / self.peak_equity if self.peak_equity > 0 else 0.0
@@ -356,6 +408,8 @@ class RLCosteer:
current_equity: float,
cash: float,
position: float,
returns_history: Optional[List[float]] = None,
timestamps: Optional[List[datetime]] = None,
) -> Dict[str, Any]:
"""
Execute one trading step.
@@ -368,14 +422,24 @@ class RLCosteer:
Available cash
position : float
Current position size
returns_history : list, optional
Historical returns for protection checks
timestamps : list, optional
Historical timestamps for protection checks
Returns
-------
dict
Step information including action taken
"""
# Get action
target_position = self.get_action(current_equity, cash, position)
# Get action with protections
target_position = self.get_action(
current_equity=current_equity,
cash=cash,
position=position,
returns_history=returns_history,
timestamps=timestamps,
)
# Record trade
trade = {
@@ -388,6 +452,13 @@ class RLCosteer:
}
self.trade_history.append(trade)
# Track equity and timestamps
self.equity_history.append(current_equity)
if timestamps:
self.timestamps_history.extend(timestamps)
else:
self.timestamps_history.append(datetime.now())
# Update peak equity
if current_equity > self.peak_equity:
self.peak_equity = current_equity
+153
View File
@@ -0,0 +1,153 @@
"""
Fallback RL implementation for users without stable-baselines3.
Provides simple rule-based trading when RL library is not available.
This ensures the Predix system works for all GitHub users, even
without the optional stable-baselines3 dependency.
The fallback implements a momentum-based strategy as a placeholder
for proper RL algorithms.
"""
import numpy as np
from typing import Optional, Any, Dict
class SimpleRLFallback:
"""
Simple momentum-based trading as fallback when RL library unavailable.
This is NOT a real RL algorithm. It provides basic functionality
so the system doesn't break when stable-baselines3 is not installed.
Strategy:
- Positive momentum -> Long position
- Negative momentum -> Short position
- Zero momentum -> Hold
Parameters
----------
window_size : int
Lookback window for momentum calculation
momentum_threshold : float
Threshold for entering positions (absolute value)
max_position : float
Maximum position size (-1 to 1)
"""
def __init__(
self,
window_size: int = 20,
momentum_threshold: float = 0.0,
max_position: float = 1.0,
) -> None:
self.window_size = window_size
self.momentum_threshold = momentum_threshold
self.max_position = max_position
self.algorithm = "FALLBACK" # Identify as fallback
self.model = self # Self-reference for compatibility
self.is_trained = True # Always "trained"
def predict(
self,
observation: np.ndarray,
deterministic: bool = True,
) -> np.ndarray:
"""
Predict action from observation using momentum strategy.
Parameters
----------
observation : np.ndarray
Observation vector (first window_size elements are prices)
deterministic : bool
Ignored (for API compatibility)
Returns
-------
np.ndarray
Action (-1 to 1)
"""
# Extract prices from observation (first window_size elements)
price_length = min(self.window_size, len(observation))
prices = observation[:price_length]
# Calculate momentum
if len(prices) < 2 or prices[0] == 0:
return np.array([0.0])
momentum = (prices[-1] - prices[0]) / prices[0]
# Apply threshold
if abs(momentum) < self.momentum_threshold:
return np.array([0.0])
# Scale to position size
position = np.clip(momentum, -self.max_position, self.max_position)
return np.array([position])
def learn(
self,
total_timesteps: int = 100000,
*args: Any,
**kwargs: Any,
) -> None:
"""
No-op for compatibility with RLTradingAgent.train().
The fallback doesn't actually train, it's a fixed strategy.
"""
pass
def save(self, path: str) -> None:
"""Save fallback config (no-op for compatibility)."""
import json
from pathlib import Path
config = {
"algorithm": "FALLBACK",
"window_size": self.window_size,
"momentum_threshold": self.momentum_threshold,
"max_position": self.max_position,
}
path_obj = Path(path)
path_obj.parent.mkdir(parents=True, exist_ok=True)
# Save as JSON instead of model file
with open(path_obj.with_suffix('.json'), 'w') as f:
json.dump(config, f, indent=2)
def load(self, path: str) -> None:
"""Load fallback config (no-op for compatibility)."""
# Fallback doesn't need to load anything
pass
@staticmethod
def is_available() -> bool:
"""
Check if stable-baselines3 is available.
Returns
-------
bool
True if stable-baselines3 is installed
"""
try:
import stable_baselines3 # noqa: F401
return True
except ImportError:
return False
@staticmethod
def get_recommendation() -> str:
"""
Get recommendation for installing RL dependencies.
Returns
-------
str
Installation command
"""
return "pip install stable-baselines3[extra] gymnasium"