mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-28 16:07:46 +00:00
2136741eaa
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
506 lines
16 KiB
Python
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)
|