| """ |
| Advanced Reward Function |
| Profit-factor optimized, regime-aware reward calculation. |
| """ |
|
|
| import numpy as np |
| from typing import Dict, List, Optional, Tuple |
| from collections import deque |
| import logging |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class AdvancedRewardCalculator: |
| """ |
| Sophisticated reward function designed to maximize profitability. |
| |
| Key innovations: |
| 1. Profit factor reward (gross profit / gross loss) |
| 2. Win rate bonus |
| 3. Regime-aware rewards (trend-following vs mean-reversion) |
| 4. Trade quality scoring |
| 5. Holding period optimization |
| """ |
| |
| def __init__( |
| self, |
| initial_balance: float = 10000.0, |
| profit_factor_weight: float = 0.4, |
| win_rate_weight: float = 0.3, |
| sharpe_weight: float = 0.3, |
| min_trades_for_stats: int = 10, |
| holding_bonus_steps: int = 4, |
| ): |
| self.initial_balance = initial_balance |
| self.profit_factor_weight = profit_factor_weight |
| self.win_rate_weight = win_rate_weight |
| self.sharpe_weight = sharpe_weight |
| self.min_trades_for_stats = min_trades_for_stats |
| self.holding_bonus_steps = holding_bonus_steps |
| |
| self.reset(initial_balance) |
| |
| def reset(self, initial_balance: float = None): |
| """Reset for new episode.""" |
| if initial_balance: |
| self.initial_balance = initial_balance |
| |
| self.portfolio_values = [self.initial_balance] |
| self.returns = [] |
| |
| |
| self.trades: List[Dict] = [] |
| self.current_position_start = None |
| self.current_position_type = 0 |
| self.steps_in_position = 0 |
| |
| |
| self.gross_profit = 0.0 |
| self.gross_loss = 0.0 |
| self.winning_trades = 0 |
| self.losing_trades = 0 |
| |
| |
| self.recent_returns = deque(maxlen=24) |
| |
| def calculate_reward( |
| self, |
| step_return: float, |
| portfolio_value: float, |
| position: int, |
| action_taken: int, |
| trade_pnl: Optional[float] = None, |
| market_features: Optional[Dict] = None, |
| ) -> float: |
| """ |
| Calculate sophisticated reward. |
| |
| Args: |
| step_return: Return this step from holding position |
| portfolio_value: Current portfolio value |
| position: Current position (-1, 0, 1) |
| action_taken: Action taken (0=hold, 1=buy, 2=sell) |
| trade_pnl: P&L from closed trade if any |
| market_features: Optional market features for regime-aware rewards |
| |
| Returns: |
| Reward value |
| """ |
| self.portfolio_values.append(portfolio_value) |
| self.returns.append(step_return) |
| self.recent_returns.append(step_return) |
| |
| reward = 0.0 |
| |
| |
| |
| reward += step_return * 100 |
| |
| |
| if trade_pnl is not None: |
| self._record_trade(trade_pnl) |
| reward += self._calculate_trade_reward(trade_pnl) |
| |
| |
| reward += self._calculate_position_reward(position, action_taken, step_return, market_features) |
| |
| |
| reward += self._calculate_risk_reward(portfolio_value) |
| |
| |
| if len(self.trades) >= self.min_trades_for_stats: |
| profit_factor = self._calculate_profit_factor() |
| if profit_factor > 1.5: |
| reward += 0.1 * (profit_factor - 1) |
| elif profit_factor < 0.5: |
| reward -= 0.2 |
| |
| |
| win_rate = self._calculate_win_rate() |
| if self.winning_trades + self.losing_trades >= self.min_trades_for_stats: |
| if win_rate > 0.55: |
| reward += 0.1 * (win_rate - 0.5) |
| elif win_rate < 0.4: |
| reward -= 0.1 |
| |
| |
| if position != 0: |
| self.steps_in_position += 1 |
| else: |
| self.steps_in_position = 0 |
| |
| return reward |
| |
| def _record_trade(self, pnl: float): |
| """Record a completed trade.""" |
| self.trades.append({ |
| 'pnl': pnl, |
| 'steps_held': self.steps_in_position, |
| }) |
| |
| if pnl > 0: |
| self.gross_profit += pnl |
| self.winning_trades += 1 |
| else: |
| self.gross_loss += abs(pnl) |
| self.losing_trades += 1 |
| |
| def _calculate_trade_reward(self, pnl: float) -> float: |
| """Calculate reward for a closed trade.""" |
| reward = 0.0 |
| |
| |
| pnl_pct = pnl / self.initial_balance |
| reward += pnl_pct * 50 |
| |
| |
| if pnl > 0 and self.steps_in_position >= self.holding_bonus_steps: |
| reward += 0.2 |
| |
| |
| if pnl < 0 and self.steps_in_position < 2: |
| reward -= 0.3 |
| |
| |
| if pnl > 0: |
| |
| estimated_risk = 0.02 * self.initial_balance |
| risk_reward = pnl / estimated_risk |
| if risk_reward > 2: |
| reward += 0.2 |
| |
| return reward |
| |
| def _calculate_position_reward( |
| self, |
| position: int, |
| action: int, |
| step_return: float, |
| market_features: Optional[Dict], |
| ) -> float: |
| """Calculate reward for position management.""" |
| reward = 0.0 |
| |
| |
| if action in [1, 2]: |
| |
| if market_features: |
| |
| trend = market_features.get('trend_strength', 0.5) |
| if action == 1 and trend > 0.6: |
| reward += 0.15 |
| elif action == 2 and trend < 0.4: |
| reward += 0.15 |
| |
| |
| position_in_range = market_features.get('position_in_range', 0.5) |
| if action == 1 and position_in_range < 0.3: |
| reward += 0.1 |
| elif action == 2 and position_in_range > 0.7: |
| reward += 0.1 |
| |
| |
| vol_ratio = market_features.get('vol_ratio_24h', 1.0) |
| if vol_ratio > 1.5: |
| reward += 0.05 |
| |
| |
| if action == 0 and position != 0: |
| |
| if step_return > 0: |
| reward += 0.05 |
| |
| elif step_return < -0.01: |
| reward -= 0.1 |
| |
| |
| if position == 0: |
| |
| |
| if market_features: |
| adx = market_features.get('adx', 0) |
| if adx > 30: |
| reward -= 0.02 |
| |
| return reward |
| |
| def _calculate_risk_reward(self, portfolio_value: float) -> float: |
| """Calculate reward for risk management.""" |
| reward = 0.0 |
| |
| |
| peak_value = max(self.portfolio_values) |
| current_drawdown = (peak_value - portfolio_value) / peak_value |
| |
| if current_drawdown > 0.10: |
| reward -= 0.5 * current_drawdown |
| elif current_drawdown > 0.05: |
| reward -= 0.2 * current_drawdown |
| |
| |
| if portfolio_value > peak_value: |
| reward += 0.1 |
| |
| return reward |
| |
| def _calculate_profit_factor(self) -> float: |
| """Calculate profit factor (gross profit / gross loss).""" |
| if self.gross_loss == 0: |
| return 2.0 if self.gross_profit > 0 else 0.0 |
| return self.gross_profit / self.gross_loss |
| |
| def _calculate_win_rate(self) -> float: |
| """Calculate win rate.""" |
| total = self.winning_trades + self.losing_trades |
| if total == 0: |
| return 0.5 |
| return self.winning_trades / total |
| |
| def get_episode_metrics(self) -> Dict: |
| """Get comprehensive metrics for the episode.""" |
| portfolio_values = np.array(self.portfolio_values) |
| returns = np.array(self.returns) if self.returns else np.array([0]) |
| |
| |
| if len(returns) > 1 and returns.std() > 0: |
| sharpe = (returns.mean() / returns.std()) * np.sqrt(8760) |
| else: |
| sharpe = 0.0 |
| |
| |
| downside_returns = returns[returns < 0] |
| if len(downside_returns) > 0 and downside_returns.std() > 0: |
| sortino = (returns.mean() / downside_returns.std()) * np.sqrt(8760) |
| else: |
| sortino = sharpe |
| |
| |
| peak = np.maximum.accumulate(portfolio_values) |
| drawdown = (peak - portfolio_values) / peak |
| max_drawdown = drawdown.max() if len(drawdown) > 0 else 0.0 |
| |
| |
| total_return = (portfolio_values[-1] - self.initial_balance) / self.initial_balance |
| |
| return { |
| 'sharpe_ratio': sharpe, |
| 'sortino_ratio': sortino, |
| 'max_drawdown': max_drawdown, |
| 'total_return': total_return, |
| 'profit_factor': self._calculate_profit_factor(), |
| 'win_rate': self._calculate_win_rate(), |
| 'total_trades': len(self.trades), |
| 'winning_trades': self.winning_trades, |
| 'losing_trades': self.losing_trades, |
| 'gross_profit': self.gross_profit, |
| 'gross_loss': self.gross_loss, |
| 'avg_trade_pnl': sum(t['pnl'] for t in self.trades) / max(len(self.trades), 1), |
| 'avg_hold_time': sum(t['steps_held'] for t in self.trades) / max(len(self.trades), 1), |
| } |
|
|