Files
NexQuant/rdagent/components/coder/rl/costeer.py
T
TPTBusiness 2136741eaa 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
2026-04-03 13:53:32 +02:00

506 lines
16 KiB
Python

"""RL CoSTEER - Code generation and RL trading controller for post-training.
This module provides two main components:
1. RLCoSTEER: LLM-based code generation for RL training pipelines
2. RLCosteer: RL-based trading controller that uses trained models
to make trading decisions based on market state.
"""
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Generator, List, Optional
import numpy as np
import pandas as pd
from rdagent.components.coder.CoSTEER import CoSTEER
from rdagent.components.coder.CoSTEER.config import CoSTEERSettings
from rdagent.components.coder.CoSTEER.evaluators import (
CoSTEERMultiEvaluator,
CoSTEERSingleFeedback,
)
from rdagent.components.coder.CoSTEER.evolvable_subjects import EvolvingItem
from rdagent.components.coder.CoSTEER.knowledge_management import (
CoSTEERQueriedKnowledge,
)
from rdagent.core.evolving_agent import EvolvingStrategy, EvoStep
from rdagent.core.experiment import FBWorkspace, Task
from rdagent.core.scenario import Scenario
from rdagent.log import rdagent_logger as logger
from rdagent.oai.llm_utils import APIBackend
from rdagent.utils.agent.tpl import T
class RLCoderCoSTEERSettings(CoSTEERSettings):
"""RL Coder settings."""
pass
class RLEvolvingStrategy(EvolvingStrategy):
"""RL code generation strategy using LLM."""
def __init__(self, scen: Scenario, settings: CoSTEERSettings):
self.scen = scen
self.settings = settings
def evolve_iter(
self,
*,
evo: EvolvingItem,
queried_knowledge: CoSTEERQueriedKnowledge | None = None,
evolving_trace: list[EvoStep] = [],
**kwargs,
) -> Generator[EvolvingItem, EvolvingItem, None]:
"""Generate code for all tasks using LLM."""
for index, target_task in enumerate(evo.sub_tasks):
code = self._generate_code(target_task, evolving_trace)
if evo.sub_workspace_list[index] is None:
evo.sub_workspace_list[index] = evo.experiment_workspace
evo.sub_workspace_list[index].inject_files(**code)
evo = yield evo
return
def _generate_code(self, task: Task, evolving_trace: list[EvoStep] = []) -> dict[str, str]:
"""Generate RL training code using LLM."""
from rdagent.app.rl.conf import RL_RD_SETTING
# Get feedback from previous round
feedback = None
if evolving_trace:
last_step = evolving_trace[-1]
if hasattr(last_step, "feedback") and last_step.feedback:
feedback = str(last_step.feedback)
# Construct prompt
system_prompt = T(".prompts:rl_coder.system").r()
user_prompt = T(".prompts:rl_coder.user").r(
task_description=task.description if hasattr(task, "description") else str(task),
base_model=RL_RD_SETTING.base_model or "",
benchmark=RL_RD_SETTING.benchmark or "",
hypothesis=str(task.name) if hasattr(task, "name") else "Train RL model",
feedback=feedback,
)
# Call LLM
session = APIBackend().build_chat_session(session_system_prompt=system_prompt)
code = session.build_chat_completion(
user_prompt=user_prompt,
json_mode=False,
code_block_language="python",
)
logger.info(f"LLM generated code:\n{code[:200]}...")
return {"main.py": code}
def _mock_code(self) -> dict[str, str]:
"""Fallback mock code."""
return {"main.py": """import gymnasium as gym
from stable_baselines3 import PPO
env = gym.make("CartPole-v1")
model = PPO("MlpPolicy", env, verbose=1)
model.learn(total_timesteps=1000)
model.save("ppo_cartpole")
print("Training completed!")
"""}
class RLCoderEvaluator:
"""RL code evaluator (mock implementation)."""
def __init__(self, scen: Scenario) -> None:
self.scen = scen
def evaluate(
self,
target_task: Task,
implementation: FBWorkspace,
gt_implementation: FBWorkspace | None,
queried_knowledge: CoSTEERQueriedKnowledge | None = None,
) -> CoSTEERSingleFeedback:
"""Evaluate RL code. Currently returns mock success."""
# TODO: Implement proper evaluation logic
return CoSTEERSingleFeedback(
execution="Mock: executed successfully",
return_checking=None,
code="Mock: code looks good",
final_decision=True,
)
class RLCoSTEER(CoSTEER):
"""RL CoSTEER - orchestrates code generation and evaluation."""
def __init__(self, scen: Scenario, *args, **kwargs) -> None:
settings = RLCoderCoSTEERSettings()
eva = CoSTEERMultiEvaluator([RLCoderEvaluator(scen=scen)], scen=scen)
es = RLEvolvingStrategy(scen=scen, settings=settings)
super().__init__(
*args,
settings=settings,
eva=eva,
es=es,
scen=scen,
max_loop=1,
stop_eval_chain_on_fail=False,
with_knowledge=False,
knowledge_self_gen=False,
**kwargs,
)
# =============================================================================
# RL Trading Controller (RLCosteer)
# =============================================================================
class RLCosteer:
"""
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.
Parameters
----------
model_path : Path, optional
Path to a trained RL model file
algorithm : str
RL algorithm used ("PPO", "A2C", "SAC")
window_size : int
Lookback window for observations
max_position : float
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
--------
>>> costeer = RLCosteer(model_path=Path("models/ppo_trader.zip"))
>>> costeer.initialize(prices, indicators, initial_equity=100000)
>>> trade = costeer.step(current_equity=101000, cash=50000, position=0.0)
"""
def __init__(
self,
model_path: Optional[Path] = None,
algorithm: str = "PPO",
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
self.model: Optional[Any] = None
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():
self.load_model(model_path)
def initialize(
self,
prices: pd.Series,
indicators: Optional[pd.DataFrame] = None,
initial_equity: float = 100000.0,
) -> None:
"""Initialize costeer with market data.
Parameters
----------
prices : pd.Series
Price time series
indicators : pd.DataFrame, optional
Technical indicator DataFrame
initial_equity : float
Starting equity
"""
self.prices = prices.values.astype(np.float64)
self.indicators = indicators.values.astype(np.float32) if indicators is not None else None
self.initial_equity = initial_equity
self.current_step = self.window_size
self.is_active = True
self.peak_equity = initial_equity
def get_action(
self,
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 with protection checks.
Parameters
----------
current_equity : float
Current portfolio equity
cash : float
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)
"""
# 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
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
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
if drawdown > self.risk_limit:
return 0.0 # Force close position if risk limit exceeded
# Scale by risk appetite
risk_multiplier = 1.0 - (drawdown / self.risk_limit)
target_position *= risk_multiplier * self.max_position
return float(np.clip(target_position, -self.max_position, self.max_position))
def _build_observation(
self,
current_equity: float,
cash: float,
position: float,
) -> np.ndarray:
"""Build observation vector for RL model.
Parameters
----------
current_equity : float
Current portfolio equity
cash : float
Available cash
position : float
Current position size
Returns
-------
np.ndarray
Observation vector
"""
start = max(0, self.current_step - self.window_size)
end = self.current_step
# Price window
price_window = self.prices[start:end]
if len(price_window) > 0 and price_window[0] != 0:
price_norm = price_window / price_window[0] - 1.0
else:
price_norm = np.zeros(len(price_window))
# Pad if needed
if len(price_norm) < self.window_size:
padded = np.zeros(self.window_size)
padded[-len(price_norm):] = price_norm
price_norm = padded
# Indicators window
if self.indicators is not None:
indicators_window = self.indicators[start:end]
if len(indicators_window) < self.window_size:
padded = np.zeros((self.window_size, self.indicators.shape[1]), dtype=np.float32)
padded[-len(indicators_window):] = indicators_window
indicators_window = padded
else:
indicators_window = np.zeros((self.window_size, 0), dtype=np.float32)
# Portfolio state features
pnl = 0.0
if position != 0 and self.current_step >= 2:
prev_price = float(self.prices[self.current_step - 2])
curr_price = float(self.prices[self.current_step - 1])
if prev_price != 0:
pnl = (curr_price - prev_price) / prev_price * np.sign(position)
# Equity ratio
equity_ratio = current_equity / self.initial_equity if self.initial_equity > 0 else 1.0
observation = np.concatenate(
[
price_norm.astype(np.float32),
indicators_window.flatten(),
np.array([position, pnl, equity_ratio], dtype=np.float32),
]
)
return observation.astype(np.float32)
def step(
self,
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.
Parameters
----------
current_equity : float
Current portfolio equity
cash : float
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 with protections
target_position = self.get_action(
current_equity=current_equity,
cash=cash,
position=position,
returns_history=returns_history,
timestamps=timestamps,
)
# Record trade
trade = {
"timestamp": datetime.now(),
"step": self.current_step,
"equity": current_equity,
"position": position,
"target_position": target_position,
"action": target_position - position,
}
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
# Move to next step
self.current_step += 1
return trade
def load_model(self, path: Path) -> None:
"""Load trained RL model.
Parameters
----------
path : Path
Path to the saved model file
Raises
------
ValueError
If model loading fails
"""
try:
from stable_baselines3 import A2C, PPO, SAC
model_class = {"PPO": PPO, "A2C": A2C, "SAC": SAC}[self.algorithm]
self.model = model_class.load(str(path))
self.is_active = True
except ImportError:
raise ImportError(
"stable-baselines3 is required. Install with: pip install stable-baselines3"
)
except Exception as e:
raise ValueError(f"Failed to load model: {e}")
def get_performance(self) -> pd.DataFrame:
"""Get trading performance history.
Returns
-------
pd.DataFrame
Trade history as DataFrame
"""
return pd.DataFrame(self.trade_history)