| """ |
| Ultimate Trading Environment |
| |
| Enhanced trading environment with: |
| - Ultimate Feature Engine (150+ features) |
| - Multi-asset correlation |
| - Advanced reward shaping |
| - Regime-aware training |
| """ |
|
|
| import gymnasium as gym |
| from gymnasium import spaces |
| import numpy as np |
| import pandas as pd |
| from typing import Optional, Dict, Tuple, Any |
| import logging |
|
|
| from src.features.ultimate_features import UltimateFeatureEngine |
| from src.features.correlation_engine import CorrelationEngine, SimulatedDominanceEngine |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class UltimateTradingEnv(gym.Env): |
| """ |
| Ultimate Trading Environment for DRL. |
| |
| Features: |
| - 150+ advanced features (Wyckoff, SMC, correlations) |
| - Enhanced reward shaping |
| - Position sizing |
| - Risk management integration |
| """ |
| |
| metadata = {'render_modes': ['human']} |
| |
| def __init__( |
| self, |
| df: pd.DataFrame, |
| initial_balance: float = 10000.0, |
| lookback_window: int = 48, |
| trading_fee: float = 0.0004, |
| position_size: float = 0.25, |
| max_position: int = 1, |
| use_correlations: bool = False, |
| reward_scaling: float = 1.0, |
| stop_loss_pct: float = 0.025, |
| take_profit_pct: float = 0.05, |
| training_mode: bool = False, |
| ): |
| super().__init__() |
|
|
| self.df = df.reset_index(drop=True) |
| self.initial_balance = initial_balance |
| self.lookback_window = lookback_window |
| self.trading_fee = trading_fee |
| self.position_size = position_size |
| self.max_position = max_position |
| self.reward_scaling = reward_scaling |
| self.stop_loss_pct = stop_loss_pct |
| self.take_profit_pct = take_profit_pct |
|
|
| |
| self.feature_engine = UltimateFeatureEngine(offline_mode=training_mode) |
| self.correlation_engine = CorrelationEngine() if use_correlations else None |
| self.dominance_engine = SimulatedDominanceEngine() |
| |
| |
| logger.info("Computing ultimate features...") |
| self._precompute_features() |
| |
| |
| self.action_space = spaces.Discrete(3) |
| |
| |
| self.num_features = self.features.shape[1] |
| |
| obs_dim = self.num_features + 3 |
| self.observation_space = spaces.Box( |
| low=-np.inf, high=np.inf, shape=(obs_dim,), dtype=np.float32 |
| ) |
| |
| logger.info(f"Ultimate Environment initialized with {self.num_features} features") |
| |
| |
| self.reset() |
| |
| def _precompute_features(self): |
| """Precompute all features for the dataset.""" |
| |
| all_features = self.feature_engine.get_all_features(self.df) |
| |
| |
| dominance_features = self.dominance_engine.compute_simulated_dominance(self.df) |
| all_features.update(dominance_features) |
| |
| |
| features_df = pd.DataFrame(all_features) |
| |
| |
| features_df = features_df.fillna(0) |
| features_df = features_df.replace([np.inf, -np.inf], 0) |
| |
| |
| for col in features_df.columns: |
| if features_df[col].dtype in [np.float64, np.float32]: |
| features_df[col] = features_df[col].clip(-10, 10) |
| |
| self.features = features_df.values.astype(np.float32) |
| self.feature_names = list(features_df.columns) |
| |
| logger.info(f"Precomputed {len(self.feature_names)} features") |
| |
| def reset( |
| self, |
| seed: Optional[int] = None, |
| options: Optional[Dict] = None, |
| ) -> Tuple[np.ndarray, Dict]: |
| super().reset(seed=seed) |
| |
| |
| if options and options.get('random_start', True): |
| max_start = len(self.df) - self.lookback_window - 100 |
| self.current_step = self.np_random.integers(self.lookback_window, max(self.lookback_window + 1, max_start)) |
| else: |
| self.current_step = self.lookback_window |
| |
| |
| self.balance = self.initial_balance |
| self.position = 0 |
| self.position_price = 0.0 |
| self.position_size_units = 0.0 |
| self.steps_since_trade = 0 |
| self.position_entry_step = 0 |
| |
| |
| self.trades = [] |
| self.equity_curve = [self.initial_balance] |
| self.returns = [] |
| self.max_balance = self.initial_balance |
| |
| return self._get_observation(), {} |
| |
| def _get_observation(self) -> np.ndarray: |
| """Get current observation.""" |
| |
| features = self.features[self.current_step].copy() |
| |
| |
| current_price = self.df.iloc[self.current_step]['close'] |
| |
| |
| if self.position != 0: |
| if self.position == 1: |
| unrealized_pnl = (current_price - self.position_price) / self.position_price |
| else: |
| unrealized_pnl = (self.position_price - current_price) / self.position_price |
| else: |
| unrealized_pnl = 0.0 |
| |
| |
| balance_ratio = (self.balance - self.initial_balance) / self.initial_balance |
| |
| |
| position_info = np.array([ |
| self.position, |
| np.clip(unrealized_pnl, -0.5, 0.5), |
| np.clip(balance_ratio, -0.5, 0.5), |
| ], dtype=np.float32) |
| |
| |
| observation = np.concatenate([features, position_info]) |
| |
| return observation.astype(np.float32) |
| |
| def step(self, action: int) -> Tuple[np.ndarray, float, bool, bool, Dict]: |
| """Execute one step.""" |
| current_price = self.df.iloc[self.current_step]['close'] |
| previous_balance = self.balance |
| |
| reward = 0.0 |
| trade_made = False |
| |
| |
| lookback = min(24, self.current_step) |
| if lookback > 0 and self.current_step >= lookback: |
| past_price = self.df.iloc[self.current_step - lookback]['close'] |
| trend_pct = (current_price - past_price) / past_price |
| trend = 1 if trend_pct > 0.01 else (-1 if trend_pct < -0.01 else 0) |
| else: |
| trend = 0 |
| |
| |
| steps_in_position = self.current_step - self.position_entry_step |
| min_hold_steps = 6 |
| |
| |
| if action == 1: |
| if self.position == -1: |
| |
| if steps_in_position < min_hold_steps: |
| reward -= 0.02 * self.reward_scaling |
| pnl = self._close_position(current_price) |
| reward += pnl * self.reward_scaling |
| trade_made = True |
| |
| if self.position == 0: |
| |
| if trend == 1: |
| reward += 0.005 * self.reward_scaling |
| elif trend == -1: |
| reward -= 0.01 * self.reward_scaling |
| |
| self._open_position(current_price, 1) |
| self.position_entry_step = self.current_step |
| reward -= 0.003 * self.reward_scaling |
| trade_made = True |
| self.steps_since_trade = 0 |
| |
| elif action == 2: |
| if self.position == 1: |
| |
| if steps_in_position < min_hold_steps: |
| reward -= 0.02 * self.reward_scaling |
| pnl = self._close_position(current_price) |
| reward += pnl * self.reward_scaling |
| trade_made = True |
| |
| if self.position == 0: |
| |
| if trend == -1: |
| reward += 0.005 * self.reward_scaling |
| elif trend == 1: |
| reward -= 0.01 * self.reward_scaling |
| |
| self._open_position(current_price, -1) |
| self.position_entry_step = self.current_step |
| reward -= 0.003 * self.reward_scaling |
| trade_made = True |
| self.steps_since_trade = 0 |
| |
| |
| self.steps_since_trade += 1 |
| |
| |
| self.current_step += 1 |
| |
| |
| done = self.current_step >= len(self.df) - 1 |
| |
| |
| if self.position != 0 and not done: |
| new_price = self.df.iloc[self.current_step]['close'] |
| |
| |
| if self.position == 1: |
| pnl_pct = (new_price - self.position_price) / self.position_price |
| else: |
| pnl_pct = (self.position_price - new_price) / self.position_price |
| |
| |
| if pnl_pct <= -self.stop_loss_pct: |
| pnl = self._close_position(new_price) |
| reward += pnl * self.reward_scaling |
| reward -= 0.05 * self.reward_scaling |
| trade_made = True |
| |
| |
| elif pnl_pct >= self.take_profit_pct: |
| pnl = self._close_position(new_price) |
| reward += pnl * self.reward_scaling |
| reward += 0.10 * self.reward_scaling |
| trade_made = True |
| |
| else: |
| |
| price_change = pnl_pct |
| reward += price_change * self.position_size * 0.3 * self.reward_scaling |
| |
| |
| equity = self._calculate_equity() |
| self.equity_curve.append(equity) |
| |
| if equity > self.max_balance: |
| self.max_balance = equity |
| |
| |
| drawdown = (self.max_balance - equity) / self.max_balance |
| if drawdown > 0.05: |
| reward -= drawdown * 0.1 * self.reward_scaling |
| |
| |
| obs = self._get_observation() if not done else np.zeros(self.observation_space.shape, dtype=np.float32) |
| |
| info = { |
| 'balance': self.balance, |
| 'equity': equity, |
| 'position': self.position, |
| 'trade_made': trade_made, |
| } |
| |
| return obs, reward, done, False, info |
| |
| def _open_position(self, price: float, direction: int): |
| """Open a position.""" |
| trade_amount = self.balance * self.position_size |
| fee = trade_amount * self.trading_fee |
| |
| self.position = direction |
| self.position_price = price |
| self.position_size_units = (trade_amount - fee) / price |
| self.balance -= fee |
| |
| def _close_position(self, price: float) -> float: |
| """Close position and return P&L ratio.""" |
| if self.position == 0: |
| return 0.0 |
| |
| if self.position == 1: |
| pnl = (price - self.position_price) * self.position_size_units |
| else: |
| pnl = (self.position_price - price) * self.position_size_units |
| |
| |
| fee = abs(pnl) * self.trading_fee if pnl > 0 else 0 |
| pnl -= fee |
| |
| self.balance += pnl |
| |
| |
| self.trades.append({ |
| 'direction': 'long' if self.position == 1 else 'short', |
| 'entry': self.position_price, |
| 'exit': price, |
| 'pnl': pnl, |
| 'pnl_pct': pnl / (self.position_price * self.position_size_units), |
| }) |
| |
| |
| self.position = 0 |
| self.position_price = 0.0 |
| self.position_size_units = 0.0 |
| |
| return pnl / self.initial_balance |
| |
| def _calculate_equity(self) -> float: |
| """Calculate current equity (balance + unrealized P&L).""" |
| if self.position == 0: |
| return self.balance |
| |
| current_price = self.df.iloc[self.current_step]['close'] |
| |
| if self.position == 1: |
| unrealized = (current_price - self.position_price) * self.position_size_units |
| else: |
| unrealized = (self.position_price - current_price) * self.position_size_units |
| |
| return self.balance + unrealized |
| |
| def get_episode_metrics(self) -> Dict[str, float]: |
| """Get episode performance metrics.""" |
| equity_curve = np.array(self.equity_curve) |
| |
| |
| total_return = (equity_curve[-1] - self.initial_balance) / self.initial_balance |
| |
| |
| if len(equity_curve) > 1: |
| returns = np.diff(equity_curve) / equity_curve[:-1] |
| else: |
| returns = np.array([0.0]) |
| |
| |
| if len(returns) > 0 and np.std(returns) > 0: |
| sharpe = np.mean(returns) / np.std(returns) * np.sqrt(24 * 365) |
| else: |
| sharpe = 0.0 |
| |
| |
| downside_returns = returns[returns < 0] |
| if len(downside_returns) > 0 and np.std(downside_returns) > 0: |
| sortino = np.mean(returns) / np.std(downside_returns) * np.sqrt(24 * 365) |
| else: |
| sortino = 0.0 |
| |
| |
| peak = np.maximum.accumulate(equity_curve) |
| drawdown = (peak - equity_curve) / peak |
| max_drawdown = np.max(drawdown) |
| |
| |
| if len(self.trades) > 0: |
| wins = sum(1 for t in self.trades if t['pnl'] > 0) |
| win_rate = wins / len(self.trades) |
| avg_trade_pnl = np.mean([t['pnl'] for t in self.trades]) |
| else: |
| win_rate = 0.0 |
| avg_trade_pnl = 0.0 |
| |
| |
| gross_profit = sum(t['pnl'] for t in self.trades if t['pnl'] > 0) |
| gross_loss = abs(sum(t['pnl'] for t in self.trades if t['pnl'] < 0)) |
| profit_factor = gross_profit / gross_loss if gross_loss > 0 else 0.0 |
| |
| return { |
| 'total_return': total_return, |
| 'total_return_pct': round(total_return * 100, 2), |
| 'sharpe_ratio': sharpe, |
| 'sortino_ratio': sortino, |
| 'max_drawdown': max_drawdown, |
| 'max_drawdown_pct': round(max_drawdown * 100, 2), |
| 'total_trades': len(self.trades), |
| 'win_rate': win_rate, |
| 'profit_factor': profit_factor, |
| 'avg_trade_pnl': avg_trade_pnl, |
| 'final_balance': equity_curve[-1], |
| } |
|
|
|
|
| def create_ultimate_env(df: pd.DataFrame, **kwargs) -> UltimateTradingEnv: |
| """Factory function to create ultimate environment.""" |
| return UltimateTradingEnv(df, **kwargs) |
|
|