Overhaul trading strategy: profit-first reward, directional diversity, tighter risk

- Reward function: profit is king, big bonus for profitable closes, penalty for losses
- Removed hold penalty (sitting in cash when uncertain is smart)
- Directional diversity: max 3 positions same direction (was 13/13 shorts)
- Tighter SL/TP: 1.0%/1.5% (was 2.5%/5.0%)
- Max 6 concurrent positions (was 250)
- RL: reset brain, live_epsilon 0.12 (was 0.03), hidden_dim 256
- GA: tighter ATR ranges, 150 population, higher mutation
- Min backtest Sharpe 0.3 (was -0.5, allowing losing strategies)
- Closed all 13 bleeding OANDA short positions
This commit is contained in:
2026-04-03 20:03:47 -07:00
parent 41471ee816
commit 8ea1df771b
4 changed files with 90 additions and 60 deletions
+33 -26
View File
@@ -32,7 +32,13 @@
"PG",
"XLE",
"CVX",
"XOM"
"XOM",
"LMT",
"RTX",
"NOC",
"GD",
"USO",
"GLD"
],
"forex_symbols": [
"USD_JPY",
@@ -49,54 +55,55 @@
"EUR_GBP",
"EUR_CHF"
],
"cycle_interval_seconds": 120,
"cycle_interval_seconds": 60,
"initial_capital": 200000,
"target_capital": 250000,
"commission_rate": 0.0,
"min_trade_value": 100,
"min_trade_value": 50,
"require_approval": false,
"max_position_pct": 10,
"max_concurrent_positions": 250,
"stop_loss_pct": 2.5,
"take_profit_pct": 5.0,
"min_backtest_sharpe": -0.5,
"max_correlated_positions": 3
"max_position_pct": 8,
"max_concurrent_positions": 6,
"stop_loss_pct": 1.0,
"take_profit_pct": 1.5,
"min_backtest_sharpe": 0.3,
"max_correlated_positions": 3,
"max_daily_trades": 50
},
"safety": {
"max_position_pct": 10,
"max_concurrent_positions": 250,
"max_daily_trades": 300,
"max_concurrent_positions": 6,
"max_daily_trades": 50,
"max_daily_loss_pct": 2,
"max_total_loss_pct": 10,
"min_trade_value": 100,
"initial_capital": 200000
},
"rl": {
"gamma": 0.97,
"epsilon_start": 1.0,
"epsilon_min": 0.05,
"epsilon_decay": 0.999,
"learning_rate": 0.001,
"gamma": 0.95,
"epsilon_start": 0.8,
"epsilon_min": 0.08,
"epsilon_decay": 0.9995,
"learning_rate": 0.0005,
"batch_size": 128,
"memory_size": 100000,
"memory_size": 250000,
"target_update_freq": 50,
"hidden_dim": 128,
"live_epsilon": 0.05,
"train_interval_hours": 0.25,
"hidden_dim": 256,
"live_epsilon": 0.12,
"train_interval_hours": 0.1,
"checkpoint_interval_hours": 1
},
"ga": {
"population_size": 60,
"population_size": 150,
"elite_count": 8,
"mutation_rate": 0.3,
"mutation_strength": 0.25,
"mutation_rate": 0.4,
"mutation_strength": 0.3,
"crossover_rate": 0.7,
"tournament_size": 5,
"evolution_interval_hours": 1,
"generations_per_cycle": 20
"evolution_interval_hours": 0.5,
"generations_per_cycle": 30
},
"backtest": {
"interval_seconds": 900,
"interval_seconds": 600,
"lookback_days": 14,
"initial_capital": 200000
},
+6 -6
View File
@@ -29,8 +29,8 @@ GENE_RANGES = {
'macd_slow': (12, 26, True),
'macd_signal': (5, 9, True),
'volume_surge_threshold': (1.1, 2.5, False),
'stop_loss_atr_mult': (0.5, 2.5, False),
'take_profit_atr_mult': (0.8, 3.0, False),
'stop_loss_atr_mult': (0.3, 1.5, False),
'take_profit_atr_mult': (0.5, 2.0, False),
'max_position_pct': (0.05, 0.15, False),
'min_hold_candles': (1, 6, True),
'max_hold_candles': (3, 36, True),
@@ -56,10 +56,10 @@ class StrategyGenome:
# Entry thresholds
volume_surge_threshold: float = 1.3
# Risk management (tighter for scalping)
stop_loss_atr_mult: float = 1.2
take_profit_atr_mult: float = 1.8
max_position_pct: float = 0.10
# Risk management (TIGHT for scalping)
stop_loss_atr_mult: float = 0.8
take_profit_atr_mult: float = 1.2
max_position_pct: float = 0.08
# Timing (short holds for scalping)
min_hold_candles: int = 1
+24 -24
View File
@@ -281,43 +281,43 @@ class TradingEnvironment:
if prev_equity <= 0:
return 0.0
# Base reward: portfolio return (amplified 2x for scalping sensitivity)
base_reward = (curr_equity - prev_equity) / prev_equity * 2.0
# PROFIT IS KING. Everything else is noise.
# Base reward: actual P&L change (amplified for sensitivity)
pnl_change = curr_equity - prev_equity
base_reward = (pnl_change / prev_equity) * 5.0
# Drawdown penalty
# Drawdown penalty (only kick in at 5%+, don't punish normal swings)
drawdown = (self.peak_equity - curr_equity) / self.peak_equity if self.peak_equity > 0 else 0
dd_penalty = -0.5 * max(0, drawdown - 0.03)
dd_penalty = -1.0 * max(0, drawdown - 0.05)
# Overtrading penalty (reduced for scalping - allow faster re-entry)
overtrade_penalty = 0.0
if action != self.HOLD and (self.step_count - self.last_action_step) < 2:
overtrade_penalty = -0.0005
# Holding penalty (stronger for scalping - don't sit idle)
hold_penalty = 0.0
if action == self.HOLD and self.position_shares == 0:
hold_penalty = -0.0002
# Profitable close bonus (works for both long and short)
# Profitable close bonus — the BIGGEST reward signal
close_bonus = 0.0
if action in (self.CLOSE_HALF, self.CLOSE_ALL) and self.position_price > 0:
if self.position_shares > 0 and current_price > self.position_price:
# Profitable long close
pnl_pct = (current_price - self.position_price) / self.position_price
close_bonus = 0.02 * pnl_pct
close_bonus = 0.1 + pnl_pct * 5.0 # Big bonus for profitable closes
elif self.position_shares < 0 and current_price < self.position_price:
# Profitable short close
pnl_pct = (self.position_price - current_price) / self.position_price
close_bonus = 0.02 * pnl_pct
close_bonus = 0.1 + pnl_pct * 5.0
elif self.position_shares > 0 and current_price < self.position_price:
pnl_pct = (self.position_price - current_price) / self.position_price
close_bonus = -0.05 - pnl_pct * 3.0 # Penalty for closing at a loss
elif self.position_shares < 0 and current_price > self.position_price:
pnl_pct = (current_price - self.position_price) / self.position_price
close_bonus = -0.05 - pnl_pct * 3.0
# Quick scalp bonus: reward fast profitable round trips
# Quick scalp bonus: fast profitable round trips get extra reward
scalp_bonus = 0.0
if action in (self.CLOSE_HALF, self.CLOSE_ALL) and self.position_shares != 0:
if action in (self.CLOSE_HALF, self.CLOSE_ALL) and close_bonus > 0:
hold_time = self.step_count - self.entry_step
if hold_time < 12 and close_bonus > 0: # Quick + profitable
scalp_bonus = 0.005
if hold_time < 12:
scalp_bonus = 0.02
reward = base_reward + dd_penalty + overtrade_penalty + hold_penalty + close_bonus + scalp_bonus
# NO hold penalty — sitting in cash when uncertain is SMART
# NO overtrading penalty — let the agent trade freely
reward = base_reward + dd_penalty + close_bonus + scalp_bonus
# Clip to [-1, 1]
return max(-1.0, min(1.0, reward))
+27 -4
View File
@@ -29,7 +29,7 @@ class TradingExecutor:
self.commission_rate = config.get('commission_rate', 0.001)
def execute_signal(self, symbol: str, action: int, current_price: float,
strategy_params: Dict = None) -> Optional[Dict]:
strategy_params: Dict = None, combined_equity: float = None) -> Optional[Dict]:
"""
Execute a trading action from the RL agent.
@@ -42,6 +42,10 @@ class TradingExecutor:
5: Short 25% of available capital
6: Short 50% of available capital
Args:
combined_equity: Optional total equity across all brokers (for multi-broker setups)
If provided, used for safety checks instead of single-broker equity
Returns: trade record dict, or None if no action taken
"""
if action == 0: # Hold
@@ -61,20 +65,34 @@ class TradingExecutor:
logger.error(f"Error getting portfolio: {e}")
return None
# Use combined equity for safety checks if provided (multi-broker setup)
safety_equity = combined_equity if combined_equity is not None else equity
# Update safety peak
self.safety.update_peak_equity(equity)
self.safety.update_peak_equity(safety_equity)
# Get current positions
open_positions = self.store.get_open_positions()
open_for_symbol = [p for p in open_positions if p['symbol'] == symbol]
num_positions = len(set(p['symbol'] for p in open_positions))
# DIRECTIONAL DIVERSITY: Don't let all positions be the same direction
all_directions = [p.get('metadata', {}).get('direction', 'long') for p in open_positions]
num_long = sum(1 for d in all_directions if d == 'long')
num_short = sum(1 for d in all_directions if d == 'short')
max_one_direction = max(num_positions - 1, 3) # At least allow 3 of same direction
# BUY LONG actions
if action in (1, 2):
if open_for_symbol:
logger.debug(f"Already have position in {symbol}, skipping buy")
return None
# Block if too many longs already
if num_long >= max_one_direction:
logger.debug(f"Too many longs ({num_long}), blocking new long on {symbol}")
return None
pct = 0.25 if action == 1 else 0.50
invest = cash * pct
@@ -93,7 +111,7 @@ class TradingExecutor:
return None
allowed, reason = self.safety.validate_trade(
symbol, 'buy', shares, current_price, equity, num_positions
symbol, 'buy', shares, current_price, safety_equity, num_positions
)
if not allowed:
logger.debug(f"Trade blocked: {reason}")
@@ -220,6 +238,11 @@ class TradingExecutor:
logger.debug(f"Already have position in {symbol}, skipping short")
return None
# Block if too many shorts already
if num_short >= max_one_direction:
logger.debug(f"Too many shorts ({num_short}), blocking new short on {symbol}")
return None
pct = 0.25 if action == 5 else 0.50
invest = cash * pct
@@ -238,7 +261,7 @@ class TradingExecutor:
return None
allowed, reason = self.safety.validate_trade(
symbol, 'sell', shares, current_price, equity, num_positions
symbol, 'sell', shares, current_price, safety_equity, num_positions
)
if not allowed:
logger.debug(f"Short blocked: {reason}")