mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-29 00:17:44 +00:00
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:
@@ -1,10 +1,12 @@
|
||||
"""
|
||||
Predix Backtesting Engine - IC, Sharpe, Drawdown
|
||||
|
||||
Supports both factor-based backtesting and RL agent backtesting.
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, Any, List
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
@@ -69,6 +71,139 @@ class FactorBacktester:
|
||||
|
||||
return metrics
|
||||
|
||||
def run_rl_backtest(
|
||||
self,
|
||||
rl_agent: Any,
|
||||
prices: pd.Series,
|
||||
indicators: Optional[pd.DataFrame] = None,
|
||||
initial_balance: float = 100000.0,
|
||||
transaction_cost: float = 0.00015,
|
||||
window_size: int = 60,
|
||||
enable_protections: bool = True,
|
||||
) -> Dict:
|
||||
"""
|
||||
Run backtest with RL agent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
rl_agent : Any
|
||||
Trained RL agent (RLTradingAgent or model with predict method)
|
||||
prices : pd.Series
|
||||
Price time series for backtesting
|
||||
indicators : pd.DataFrame, optional
|
||||
Technical indicators DataFrame
|
||||
initial_balance : float
|
||||
Starting balance
|
||||
transaction_cost : float
|
||||
Transaction cost per trade
|
||||
window_size : int
|
||||
Lookback window for observations
|
||||
enable_protections : bool
|
||||
Enable trading protections
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Backtest metrics
|
||||
"""
|
||||
from rdagent.components.coder.rl import RLCosteer
|
||||
|
||||
# Create costeer with protections
|
||||
costeer = RLCosteer(
|
||||
model_path=None,
|
||||
algorithm=getattr(rl_agent, 'algorithm', 'PPO'),
|
||||
window_size=window_size,
|
||||
enable_protections=enable_protections,
|
||||
)
|
||||
|
||||
# Attach trained model directly
|
||||
if hasattr(rl_agent, 'model'):
|
||||
costeer.model = rl_agent.model
|
||||
costeer.is_active = True
|
||||
elif hasattr(rl_agent, 'predict'):
|
||||
# Agent has predict method directly
|
||||
costeer.model = rl_agent
|
||||
costeer.is_active = True
|
||||
else:
|
||||
raise ValueError("RL agent must have 'model' or 'predict' attribute")
|
||||
|
||||
# Initialize with price data
|
||||
costeer.initialize(
|
||||
prices=prices,
|
||||
indicators=indicators,
|
||||
initial_equity=initial_balance,
|
||||
)
|
||||
|
||||
# Run simulation
|
||||
equity_curve: List[float] = [initial_balance]
|
||||
position = 0.0
|
||||
cash = initial_balance
|
||||
returns_history: List[float] = []
|
||||
|
||||
price_values = prices.values if isinstance(prices, pd.Series) else np.array(prices)
|
||||
|
||||
for step in range(len(price_values) - 1):
|
||||
# Ensure costeer doesn't go beyond available data
|
||||
if costeer.current_step >= len(price_values):
|
||||
break
|
||||
|
||||
current_price = float(price_values[step])
|
||||
current_equity = cash + position * current_price
|
||||
|
||||
# Get RL action with protections
|
||||
trade_info = costeer.step(
|
||||
current_equity=current_equity,
|
||||
cash=cash,
|
||||
position=position,
|
||||
returns_history=returns_history[-100:] if returns_history else None, # Last 100 returns
|
||||
)
|
||||
|
||||
# Execute trade (simplified)
|
||||
target_position = trade_info["target_position"]
|
||||
position_change = target_position - position
|
||||
|
||||
# Calculate transaction cost
|
||||
trade_value = abs(position_change) * current_price
|
||||
cost = trade_value * transaction_cost
|
||||
|
||||
# Update position and cash
|
||||
position = target_position
|
||||
cash -= cost
|
||||
|
||||
# Calculate return for this step
|
||||
if step > 0:
|
||||
prev_price = float(price_values[step - 1]) if step > 0 else current_price
|
||||
if prev_price > 0:
|
||||
step_return = (current_price - prev_price) / prev_price * position
|
||||
returns_history.append(step_return)
|
||||
|
||||
# Calculate new equity
|
||||
new_equity = cash + position * current_price
|
||||
equity_curve.append(new_equity)
|
||||
|
||||
# Calculate metrics
|
||||
equity_series = pd.Series(equity_curve)
|
||||
returns_series = equity_series.pct_change().dropna()
|
||||
|
||||
metrics = self.metrics.calculate_all(returns_series, equity_series)
|
||||
metrics["factor_name"] = f"RL_{getattr(rl_agent, 'algorithm', 'Unknown')}"
|
||||
metrics["timestamp"] = datetime.now().isoformat()
|
||||
metrics["initial_balance"] = initial_balance
|
||||
metrics["final_equity"] = equity_curve[-1]
|
||||
metrics["total_steps"] = len(price_values) - 1
|
||||
|
||||
# Save results
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
rl_name = f"RL_{getattr(rl_agent, 'algorithm', 'Unknown')}"
|
||||
|
||||
with open(self.results_path / f"{rl_name}_{timestamp}.json", 'w') as f:
|
||||
json.dump(
|
||||
{k: (None if isinstance(v, float) and np.isnan(v) else v) for k, v in metrics.items()},
|
||||
f, indent=2
|
||||
)
|
||||
|
||||
return metrics
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Backtest Test ===")
|
||||
np.random.seed(42)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user