"""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)