normalize by unified price range
This commit is contained in:
@ -3117,87 +3117,86 @@ class DataProvider:
|
|||||||
return basic_cols # Fallback to basic OHLCV
|
return basic_cols # Fallback to basic OHLCV
|
||||||
|
|
||||||
def _normalize_features(self, df: pd.DataFrame, symbol: str = None) -> Optional[pd.DataFrame]:
|
def _normalize_features(self, df: pd.DataFrame, symbol: str = None) -> Optional[pd.DataFrame]:
|
||||||
"""Normalize features for CNN training using pivot-based bounds when available"""
|
"""Normalize features for CNN training using unified normalization across all timeframes"""
|
||||||
try:
|
try:
|
||||||
df_norm = df.copy()
|
df_norm = df.copy()
|
||||||
|
|
||||||
# Try to use pivot-based normalization if available
|
# Get unified normalization bounds for all timeframes
|
||||||
if symbol and symbol in self.pivot_bounds:
|
if symbol and symbol in self.pivot_bounds:
|
||||||
bounds = self.pivot_bounds[symbol]
|
bounds = self.pivot_bounds[symbol]
|
||||||
price_range = bounds.get_price_range()
|
price_range = bounds.get_price_range()
|
||||||
|
volume_range = bounds.volume_max - bounds.volume_min
|
||||||
|
|
||||||
# Normalize price-based features using pivot bounds
|
logger.debug(f"Using unified pivot-based normalization for {symbol} (price_range: {price_range:.2f})")
|
||||||
price_cols = ['open', 'high', 'low', 'close', 'sma_10', 'sma_20', 'sma_50',
|
|
||||||
'ema_12', 'ema_26', 'ema_50', 'bb_upper', 'bb_lower', 'bb_middle',
|
|
||||||
'keltner_upper', 'keltner_lower', 'keltner_middle', 'psar', 'vwap']
|
|
||||||
|
|
||||||
for col in price_cols:
|
|
||||||
if col in df_norm.columns:
|
|
||||||
# Use pivot bounds for normalization
|
|
||||||
df_norm[col] = (df_norm[col] - bounds.price_min) / price_range
|
|
||||||
|
|
||||||
# Normalize volume using pivot bounds
|
|
||||||
if 'volume' in df_norm.columns:
|
|
||||||
volume_range = bounds.volume_max - bounds.volume_min
|
|
||||||
if volume_range > 0:
|
|
||||||
df_norm['volume'] = (df_norm['volume'] - bounds.volume_min) / volume_range
|
|
||||||
else:
|
|
||||||
df_norm['volume'] = 0.5 # Default to middle if no volume range
|
|
||||||
|
|
||||||
logger.debug(f"Applied pivot-based normalization for {symbol}")
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Fallback to traditional normalization when pivot bounds not available
|
# Fallback: calculate unified bounds from available data
|
||||||
logger.debug("Using traditional normalization (no pivot bounds available)")
|
price_range = self._get_price_range_for_symbol(symbol) if symbol else 1000.0
|
||||||
|
volume_range = 1000000.0 # Default volume range
|
||||||
for col in df_norm.columns:
|
logger.debug(f"Using fallback unified normalization for {symbol} (price_range: {price_range:.2f})")
|
||||||
if col in ['open', 'high', 'low', 'close', 'sma_10', 'sma_20', 'sma_50',
|
|
||||||
'ema_12', 'ema_26', 'ema_50', 'bb_upper', 'bb_lower', 'bb_middle',
|
# UNIFIED NORMALIZATION: All timeframes use the same normalization range
|
||||||
'keltner_upper', 'keltner_lower', 'keltner_middle', 'psar', 'vwap']:
|
# This preserves relationships between different timeframes
|
||||||
# Price-based indicators: normalize by close price
|
|
||||||
|
# Price-based features (OHLCV + indicators)
|
||||||
|
price_cols = ['open', 'high', 'low', 'close', 'sma_10', 'sma_20', 'sma_50',
|
||||||
|
'ema_12', 'ema_26', 'ema_50', 'bb_upper', 'bb_lower', 'bb_middle',
|
||||||
|
'keltner_upper', 'keltner_lower', 'keltner_middle', 'psar', 'vwap']
|
||||||
|
|
||||||
|
for col in price_cols:
|
||||||
|
if col in df_norm.columns:
|
||||||
|
if symbol and symbol in self.pivot_bounds:
|
||||||
|
# Use pivot bounds for unified normalization
|
||||||
|
df_norm[col] = (df_norm[col] - bounds.price_min) / price_range
|
||||||
|
else:
|
||||||
|
# Fallback: normalize by current price range
|
||||||
if 'close' in df_norm.columns:
|
if 'close' in df_norm.columns:
|
||||||
base_price = df_norm['close'].iloc[-1] # Use latest close as reference
|
base_price = df_norm['close'].iloc[-1]
|
||||||
if base_price > 0:
|
if base_price > 0:
|
||||||
df_norm[col] = df_norm[col] / base_price
|
df_norm[col] = df_norm[col] / base_price
|
||||||
|
|
||||||
elif col == 'volume':
|
|
||||||
# Volume: normalize by its own rolling mean
|
|
||||||
volume_mean = df_norm[col].rolling(window=min(20, len(df_norm))).mean().iloc[-1]
|
|
||||||
if volume_mean > 0:
|
|
||||||
df_norm[col] = df_norm[col] / volume_mean
|
|
||||||
|
|
||||||
# Normalize indicators that have standard ranges (regardless of pivot bounds)
|
# Volume normalization (unified across timeframes)
|
||||||
|
if 'volume' in df_norm.columns:
|
||||||
|
if symbol and symbol in self.pivot_bounds and volume_range > 0:
|
||||||
|
df_norm['volume'] = (df_norm['volume'] - bounds.volume_min) / volume_range
|
||||||
|
else:
|
||||||
|
# Fallback: normalize by rolling mean
|
||||||
|
volume_mean = df_norm['volume'].rolling(window=min(20, len(df_norm))).mean().iloc[-1]
|
||||||
|
if volume_mean > 0:
|
||||||
|
df_norm['volume'] = df_norm['volume'] / volume_mean
|
||||||
|
else:
|
||||||
|
df_norm['volume'] = 0.5
|
||||||
|
|
||||||
|
# Standard range indicators (already 0-1 or 0-100)
|
||||||
for col in df_norm.columns:
|
for col in df_norm.columns:
|
||||||
if col in ['rsi_14', 'rsi_7', 'rsi_21']:
|
if col in ['rsi_14', 'rsi_7', 'rsi_21']:
|
||||||
# RSI: already 0-100, normalize to 0-1
|
# RSI: 0-100 -> 0-1
|
||||||
df_norm[col] = df_norm[col] / 100.0
|
df_norm[col] = df_norm[col] / 100.0
|
||||||
|
|
||||||
elif col in ['stoch_k', 'stoch_d']:
|
elif col in ['stoch_k', 'stoch_d']:
|
||||||
# Stochastic: already 0-100, normalize to 0-1
|
# Stochastic: 0-100 -> 0-1
|
||||||
df_norm[col] = df_norm[col] / 100.0
|
df_norm[col] = df_norm[col] / 100.0
|
||||||
|
|
||||||
elif col == 'williams_r':
|
elif col == 'williams_r':
|
||||||
# Williams %R: -100 to 0, normalize to 0-1
|
# Williams %R: -100 to 0 -> 0-1
|
||||||
df_norm[col] = (df_norm[col] + 100) / 100.0
|
df_norm[col] = (df_norm[col] + 100) / 100.0
|
||||||
|
|
||||||
elif col in ['macd', 'macd_signal', 'macd_histogram']:
|
elif col in ['macd', 'macd_signal', 'macd_histogram']:
|
||||||
# MACD: normalize by ATR or close price
|
# MACD: normalize by unified price range
|
||||||
if 'atr' in df_norm.columns and df_norm['atr'].iloc[-1] > 0:
|
if symbol and symbol in self.pivot_bounds:
|
||||||
df_norm[col] = df_norm[col] / df_norm['atr'].iloc[-1]
|
df_norm[col] = df_norm[col] / price_range
|
||||||
elif 'close' in df_norm.columns and df_norm['close'].iloc[-1] > 0:
|
elif 'close' in df_norm.columns and df_norm['close'].iloc[-1] > 0:
|
||||||
df_norm[col] = df_norm[col] / df_norm['close'].iloc[-1]
|
df_norm[col] = df_norm[col] / df_norm['close'].iloc[-1]
|
||||||
|
|
||||||
elif col in ['bb_width', 'bb_percent', 'price_position', 'trend_strength',
|
elif col in ['bb_width', 'bb_percent', 'price_position', 'trend_strength',
|
||||||
'momentum_composite', 'volatility_regime', 'pivot_price_position',
|
'momentum_composite', 'volatility_regime', 'pivot_price_position',
|
||||||
'pivot_support_distance', 'pivot_resistance_distance']:
|
'pivot_support_distance', 'pivot_resistance_distance']:
|
||||||
# Already normalized indicators: ensure 0-1 range
|
# Already normalized: ensure 0-1 range
|
||||||
df_norm[col] = np.clip(df_norm[col], 0, 1)
|
df_norm[col] = np.clip(df_norm[col], 0, 1)
|
||||||
|
|
||||||
elif col in ['atr', 'true_range']:
|
elif col in ['atr', 'true_range']:
|
||||||
# Volatility indicators: normalize by close price or pivot range
|
# Volatility: normalize by unified price range
|
||||||
if symbol and symbol in self.pivot_bounds:
|
if symbol and symbol in self.pivot_bounds:
|
||||||
bounds = self.pivot_bounds[symbol]
|
df_norm[col] = df_norm[col] / price_range
|
||||||
df_norm[col] = df_norm[col] / bounds.get_price_range()
|
|
||||||
elif 'close' in df_norm.columns and df_norm['close'].iloc[-1] > 0:
|
elif 'close' in df_norm.columns and df_norm['close'].iloc[-1] > 0:
|
||||||
df_norm[col] = df_norm[col] / df_norm['close'].iloc[-1]
|
df_norm[col] = df_norm[col] / df_norm['close'].iloc[-1]
|
||||||
|
|
||||||
@ -3210,12 +3209,19 @@ class DataProvider:
|
|||||||
else:
|
else:
|
||||||
df_norm[col] = 0
|
df_norm[col] = 0
|
||||||
|
|
||||||
# Replace inf/-inf with 0
|
# Clean up any invalid values
|
||||||
df_norm = df_norm.replace([np.inf, -np.inf], 0)
|
df_norm = df_norm.replace([np.inf, -np.inf], 0)
|
||||||
|
|
||||||
# Fill any remaining NaN values
|
|
||||||
df_norm = df_norm.fillna(0)
|
df_norm = df_norm.fillna(0)
|
||||||
|
|
||||||
|
# Ensure all values are in reasonable range for neural networks
|
||||||
|
df_norm = np.clip(df_norm, -10, 10)
|
||||||
|
|
||||||
|
return df_norm
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in unified feature normalization: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
return df_norm
|
return df_norm
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -3541,6 +3541,7 @@ class TradingOrchestrator:
|
|||||||
"""
|
"""
|
||||||
Calculate sophisticated reward based on prediction accuracy, confidence, and price movement magnitude
|
Calculate sophisticated reward based on prediction accuracy, confidence, and price movement magnitude
|
||||||
Now considers position status and current P&L when evaluating decisions
|
Now considers position status and current P&L when evaluating decisions
|
||||||
|
NOISE REDUCTION: Treats neutral/low-confidence signals as HOLD to reduce training noise
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
predicted_action: The predicted action ('BUY', 'SELL', 'HOLD')
|
predicted_action: The predicted action ('BUY', 'SELL', 'HOLD')
|
||||||
@ -3556,8 +3557,15 @@ class TradingOrchestrator:
|
|||||||
tuple: (reward, was_correct)
|
tuple: (reward, was_correct)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
# NOISE REDUCTION: Treat low-confidence signals as HOLD
|
||||||
|
confidence_threshold = 0.6 # Only consider BUY/SELL if confidence > 60%
|
||||||
|
if prediction_confidence < confidence_threshold:
|
||||||
|
predicted_action = "HOLD"
|
||||||
|
logger.debug(f"Low confidence ({prediction_confidence:.2f}) - treating as HOLD for noise reduction")
|
||||||
|
|
||||||
# Base thresholds for determining correctness
|
# Base thresholds for determining correctness
|
||||||
movement_threshold = 0.1 # 0.1% minimum movement to consider significant
|
movement_threshold = 0.15 # Increased from 0.1% to 0.15% for stronger signals
|
||||||
|
strong_movement_threshold = 0.5 # 0.5% for strong movements
|
||||||
|
|
||||||
# Determine current position status if not provided
|
# Determine current position status if not provided
|
||||||
if has_position is None and symbol:
|
if has_position is None and symbol:
|
||||||
@ -3573,58 +3581,62 @@ class TradingOrchestrator:
|
|||||||
directional_accuracy = 0.0
|
directional_accuracy = 0.0
|
||||||
|
|
||||||
if predicted_action == "BUY":
|
if predicted_action == "BUY":
|
||||||
|
# BUY signals need stronger confirmation for higher rewards
|
||||||
was_correct = price_change_pct > movement_threshold
|
was_correct = price_change_pct > movement_threshold
|
||||||
directional_accuracy = max(
|
if price_change_pct > strong_movement_threshold:
|
||||||
0, price_change_pct
|
directional_accuracy = price_change_pct * 2.0 # Bonus for strong moves
|
||||||
) # Positive for upward movement
|
else:
|
||||||
|
directional_accuracy = max(0, price_change_pct) # Standard reward
|
||||||
|
|
||||||
elif predicted_action == "SELL":
|
elif predicted_action == "SELL":
|
||||||
|
# SELL signals need stronger confirmation for higher rewards
|
||||||
was_correct = price_change_pct < -movement_threshold
|
was_correct = price_change_pct < -movement_threshold
|
||||||
directional_accuracy = max(
|
if price_change_pct < -strong_movement_threshold:
|
||||||
0, -price_change_pct
|
directional_accuracy = abs(price_change_pct) * 2.0 # Bonus for strong moves
|
||||||
) # Positive for downward movement
|
else:
|
||||||
|
directional_accuracy = max(0, -price_change_pct) # Standard reward
|
||||||
|
|
||||||
elif predicted_action == "HOLD":
|
elif predicted_action == "HOLD":
|
||||||
# HOLD evaluation now considers position status AND current P&L
|
# HOLD evaluation with noise reduction - smaller rewards to reduce training noise
|
||||||
if has_position:
|
if has_position:
|
||||||
# If we have a position, HOLD evaluation depends on P&L and price movement
|
# If we have a position, HOLD evaluation depends on P&L and price movement
|
||||||
if current_position_pnl > 0: # Currently profitable position
|
if current_position_pnl > 0: # Currently profitable position
|
||||||
# Holding a profitable position is good if price continues favorably
|
# Holding a profitable position is good if price continues favorably
|
||||||
if price_change_pct > 0: # Price went up while holding profitable position - excellent
|
if price_change_pct > 0: # Price went up while holding profitable position - excellent
|
||||||
was_correct = True
|
was_correct = True
|
||||||
directional_accuracy = price_change_pct * 1.5 # Bonus for holding winners
|
directional_accuracy = price_change_pct * 0.8 # Reduced from 1.5 to reduce noise
|
||||||
elif abs(price_change_pct) < movement_threshold: # Price stable - good
|
elif abs(price_change_pct) < movement_threshold: # Price stable - good
|
||||||
was_correct = True
|
was_correct = True
|
||||||
directional_accuracy = movement_threshold + (current_position_pnl / 100.0) # Reward based on existing profit
|
directional_accuracy = movement_threshold * 0.5 # Reduced reward to reduce noise
|
||||||
else: # Price dropped while holding profitable position - still okay but less reward
|
else: # Price dropped while holding profitable position - still okay but less reward
|
||||||
was_correct = True
|
was_correct = True
|
||||||
directional_accuracy = max(0, (current_position_pnl / 100.0) - abs(price_change_pct) * 0.5)
|
directional_accuracy = max(0, (current_position_pnl / 100.0) - abs(price_change_pct) * 0.3)
|
||||||
elif current_position_pnl < 0: # Currently losing position
|
elif current_position_pnl < 0: # Currently losing position
|
||||||
# Holding a losing position is generally bad - should consider closing
|
# Holding a losing position is generally bad - should consider closing
|
||||||
if price_change_pct > movement_threshold: # Price recovered - good hold
|
if price_change_pct > movement_threshold: # Price recovered - good hold
|
||||||
was_correct = True
|
was_correct = True
|
||||||
directional_accuracy = price_change_pct * 0.8 # Reduced reward for recovery
|
directional_accuracy = price_change_pct * 0.6 # Reduced reward
|
||||||
else: # Price continued down or stayed flat - bad hold
|
else: # Price continued down or stayed flat - bad hold
|
||||||
was_correct = False
|
was_correct = False
|
||||||
# Penalty proportional to loss magnitude
|
# Penalty proportional to loss magnitude
|
||||||
directional_accuracy = abs(current_position_pnl / 100.0) * 0.5 # Penalty for holding losers
|
directional_accuracy = abs(current_position_pnl / 100.0) * 0.3 # Reduced penalty
|
||||||
else: # Breakeven position
|
else: # Breakeven position
|
||||||
# Standard HOLD evaluation for breakeven positions
|
# Standard HOLD evaluation for breakeven positions
|
||||||
if abs(price_change_pct) < movement_threshold: # Price stable - good
|
if abs(price_change_pct) < movement_threshold: # Price stable - good
|
||||||
was_correct = True
|
was_correct = True
|
||||||
directional_accuracy = movement_threshold - abs(price_change_pct)
|
directional_accuracy = movement_threshold * 0.4 # Reduced reward
|
||||||
else: # Price moved significantly - missed opportunity
|
else: # Price moved significantly - missed opportunity
|
||||||
was_correct = False
|
was_correct = False
|
||||||
directional_accuracy = max(0, movement_threshold - abs(price_change_pct)) * 0.7
|
directional_accuracy = max(0, movement_threshold - abs(price_change_pct)) * 0.5
|
||||||
else:
|
else:
|
||||||
# If we don't have a position, HOLD is correct if price stayed relatively stable
|
# If we don't have a position, HOLD is correct if price stayed relatively stable
|
||||||
was_correct = abs(price_change_pct) < movement_threshold
|
was_correct = abs(price_change_pct) < movement_threshold
|
||||||
directional_accuracy = max(
|
directional_accuracy = max(0, movement_threshold - abs(price_change_pct)) * 0.4 # Reduced reward
|
||||||
0, movement_threshold - abs(price_change_pct)
|
|
||||||
) # Positive for stability
|
|
||||||
|
|
||||||
# Calculate magnitude-based multiplier (higher rewards for larger correct movements)
|
# Calculate magnitude-based multiplier (higher rewards for larger correct movements)
|
||||||
magnitude_multiplier = min(
|
magnitude_multiplier = min(
|
||||||
abs(price_change_pct) / 2.0, 3.0
|
abs(price_change_pct) / 2.0, 2.5 # Reduced from 3.0 to 2.5 to reduce noise
|
||||||
) # Cap at 3x for 6% moves
|
) # Cap at 2.5x for 5% moves
|
||||||
|
|
||||||
# Calculate confidence-based reward adjustment
|
# Calculate confidence-based reward adjustment
|
||||||
if was_correct:
|
if was_correct:
|
||||||
|
Reference in New Issue
Block a user