diff --git a/main_live.py b/main_live.py index 276eb34..eceaf60 100644 --- a/main_live.py +++ b/main_live.py @@ -61,7 +61,7 @@ from src.auto_trainer import AutoTrainer, create_auto_trainer from src.telegram_notifier import TelegramNotifier, create_telegram_notifier from src.smart_risk_manager import SmartRiskManager, create_smart_risk_manager from src.dynamic_confidence import DynamicConfidenceManager, create_dynamic_confidence -from src.news_agent import NewsAgent, create_news_agent, MarketCondition +# from src.news_agent import NewsAgent, create_news_agent, MarketCondition # DISABLED from src.trade_logger import TradeLogger, get_trade_logger @@ -129,11 +129,14 @@ class TradingBot: model_path="models/xgboost_model.pkl", ) - # Initialize Smart Position Manager - ULTRA SAFE MODE + # Initialize Smart Position Manager - ATR-ADAPTIVE (#24B) self.position_manager = SmartPositionManager( - breakeven_pips=5.0, # Move to breakeven after 5 pips profit - trail_start_pips=10.0, # Start trailing after 10 pips - trail_step_pips=5.0, # Trail by 5 pips + breakeven_pips=30.0, # Fallback if ATR unavailable + trail_start_pips=50.0, # Fallback if ATR unavailable + trail_step_pips=30.0, # Fallback if ATR unavailable + atr_be_mult=2.0, # Breakeven = ATR * 2.0 (#24B) + atr_trail_start_mult=4.0, # Trail start = ATR * 4.0 (#24B) + atr_trail_step_mult=3.0, # Trail step = ATR * 3.0 (#24B) min_profit_to_protect=5.0, # Protect profits > $5 max_drawdown_from_peak=50.0, # Allow 50% drawdown (we use tiny lots) # Smart Market Close Handler @@ -157,15 +160,9 @@ class TradingBot: # Initialize Telegram Notifier - smart notifications self.telegram = create_telegram_notifier() - # Initialize News Agent - economic calendar monitoring (NO BLOCKING) - # Based on comprehensive backtest (29 trades, 62.1% WR, $178 profit): - # - News filter COSTS us $178.15 profit - # - ML model already handles market volatility well - # - Keep monitoring for logging but DO NOT block trades - self.news_agent = create_news_agent( - news_buffer_minutes=0, # No blocking - high_impact_buffer_minutes=0, # No blocking - ML model handles volatility - ) + # News Agent DISABLED - backtest proved it costs $178 profit + # ML model already handles volatility well + self.news_agent = None # Initialize Trade Logger - for ML auto-training self.trade_logger = get_trade_logger() @@ -391,6 +388,10 @@ class TradingBot: "cooldownSeconds": self.config.thresholds.trade_cooldown_seconds, "symbol": self.config.symbol, }, + "h1Bias": getattr(self, "_h1_bias_cache", "NEUTRAL"), + "dynamicThreshold": getattr(self, "_last_dynamic_threshold", self.config.ml.confidence_threshold), + "marketQuality": getattr(self, "_last_market_quality", "unknown"), + "marketScore": getattr(self, "_last_market_score", 0), } # Atomic write (write to temp then rename) @@ -434,10 +435,6 @@ class TradingBot: logger.info(f"Session: {session_status['current_session']} ({session_status['volatility']} vol)") logger.info(f"Can Trade: {session_status['can_trade']} - {session_status['reason']}") - # Show news agent status - news_can_trade, news_reason, _ = self.news_agent.should_trade() - logger.info(f"News Agent: {'SAFE' if news_can_trade else 'BLOCKED'} - {news_reason}") - # Track daily start balance self._daily_start_balance = balance self._start_time = datetime.now() @@ -445,14 +442,13 @@ class TradingBot: # Send Telegram startup notification ml_status = f"Loaded ({len(self.ml_model.feature_names)} features)" if self.ml_model.fitted else "Not loaded" - news_status = "SAFE" if news_can_trade else "BLOCKED" await self.telegram.send_startup_message( symbol=self.config.symbol, capital=self.config.capital, balance=balance, mode=self.config.capital_mode.value, ml_model_status=ml_status, - news_status=news_status, + news_status="DISABLED", ) except Exception as e: @@ -925,18 +921,7 @@ class TradingBot: self._current_session_multiplier = session_multiplier self._is_sydney_session = "Sydney" in session_reason or session_multiplier == 0.5 - # 7.6 NEWS AGENT MONITORING (NO BLOCKING) - # Based on backtest analysis: News filter COSTS $178 profit - # ML model already handles volatility well - no need to block - can_trade_news, news_reason, news_lot_mult = self.news_agent.should_trade() - - # Log news status for monitoring but DO NOT block trades - if not can_trade_news and self._loop_count % 300 == 0: - logger.info(f"News Agent: HIGH IMPACT NEWS - {news_reason} (trading allowed)") - - # Note: We no longer block trades during news events - # Backtest showed trades during news have 62.1% win rate (vs 64.9% normal) - # The $178 profit opportunity outweighs the minimal risk difference + # 7.6 NEWS AGENT - DISABLED (backtest: costs $178 profit, ML handles volatility) # 7.7 H1 Multi-Timeframe Bias (Fix 5) # Fetch H1 data and determine higher-TF bias for M15 signal filtering @@ -977,44 +962,20 @@ class TradingBot: if final_signal is None: return - # 10.1 H1 Multi-Timeframe Filter (Fix 5) - # Block M15 signal if it contradicts H1 bias + # 10.1 H1 Multi-Timeframe Filter - DISABLED (SMC-only mode) + # H1 bias still logged for dashboard but does NOT block trades if h1_bias != "NEUTRAL": - if final_signal.signal_type == "BUY" and h1_bias == "BEARISH": - logger.info(f"Skip BUY: H1 bias is BEARISH (counter-trend)") - return - if final_signal.signal_type == "SELL" and h1_bias == "BULLISH": - logger.info(f"Skip SELL: H1 bias is BULLISH (counter-trend)") - return - # Boost confidence when aligned - if (final_signal.signal_type == "BUY" and h1_bias == "BULLISH") or \ - (final_signal.signal_type == "SELL" and h1_bias == "BEARISH"): - final_signal = SMCSignal( - signal_type=final_signal.signal_type, - entry_price=final_signal.entry_price, - stop_loss=final_signal.stop_loss, - take_profit=final_signal.take_profit, - confidence=min(final_signal.confidence * 1.1, 0.95), - reason=f"{final_signal.reason} | H1-ALIGNED", - ) + logger.info(f"H1 Bias: {h1_bias} (monitoring only, not blocking)") # 10.5 Check trade cooldown if self._last_trade_time: time_since_last = (datetime.now() - self._last_trade_time).total_seconds() if time_since_last < self._trade_cooldown_seconds: - logger.debug(f"Trade cooldown: {self._trade_cooldown_seconds - time_since_last:.0f}s remaining") + logger.info(f"Trade cooldown: {self._trade_cooldown_seconds - time_since_last:.0f}s remaining") return - # 10.6 PULLBACK FILTER - Prevent entry during temporary retracements - pullback_ok, pullback_reason = self._check_pullback_filter( - df=df, - signal_direction=final_signal.signal_type, - current_price=current_price, - ) - if not pullback_ok: - if self._loop_count % 30 == 0: # Log every 30 loops - logger.info(f"Pullback Filter: {pullback_reason}") - return + # 10.6 PULLBACK FILTER - DISABLED (SMC-only mode) + # SMC structure already validates entry zones # 11. SMART RISK CHECK - Ultra safe mode self.smart_risk.check_new_day() @@ -1111,6 +1072,9 @@ class TradingBot: # Get dynamic threshold dynamic_threshold = market_analysis.confidence_threshold + self._last_dynamic_threshold = dynamic_threshold + self._last_market_quality = market_analysis.quality.value + self._last_market_score = market_analysis.score # Log dynamic analysis periodically if self._loop_count % 60 == 0: @@ -1140,80 +1104,12 @@ class TradingBot: return None # ============================================================ - # IMPROVED SIGNAL LOGIC v3 - With ML Threshold & Confirmation + # SIGNAL LOGIC v4 - SMC-Only (ML DISABLED) # ============================================================ golden_marker = "[GOLDEN] " if is_golden_time else "" if smc_signal is not None: - # === IMPROVEMENT 1: ML Confidence Threshold === - # Based on backtest tuning (Jan 2025 - Feb 2026): - # - 50% threshold: 485 trades, 61.6% WR, $3120 profit, PF 2.02 - # - 55% threshold: 306 trades, 59.5% WR, $1443 profit, PF 1.74 - # OPTIMAL: 50% threshold (more trades, higher WR, better profit) - ml_min_threshold = 0.50 - if ml_prediction.confidence < ml_min_threshold: - if self._loop_count % 60 == 0: - logger.info(f"Skip: ML uncertain ({ml_prediction.confidence:.0%} < {ml_min_threshold:.0%}) - waiting for clearer signal") - return None - - # Check if ML strongly disagrees (>65% opposite) - ml_strongly_disagrees = ( - (smc_signal.signal_type == "BUY" and ml_prediction.signal == "SELL" and ml_prediction.confidence > 0.65) or - (smc_signal.signal_type == "SELL" and ml_prediction.signal == "BUY" and ml_prediction.confidence > 0.65) - ) - - if ml_strongly_disagrees: - if self._loop_count % 60 == 0: - logger.info(f"Skip: ML strongly disagrees ({ml_prediction.signal} {ml_prediction.confidence:.0%}) vs SMC {smc_signal.signal_type}") - return None - - # === IMPROVEMENT 1.5: SELL Filter (OPTIMIZED) === - # SELL signals historically have lower win rate than BUY - # Require ML agreement and higher confidence for SELL - if smc_signal.signal_type == "SELL": - if ml_prediction.signal != "SELL": - if self._loop_count % 60 == 0: - logger.info(f"Skip SELL: ML does not agree ({ml_prediction.signal} {ml_prediction.confidence:.0%})") - return None - if ml_prediction.confidence < 0.55: - if self._loop_count % 60 == 0: - logger.info(f"Skip SELL: ML confidence too low ({ml_prediction.confidence:.0%} < 55%)") - return None - - # === IMPROVEMENT 2: Signal Confirmation (Entry Delay) === - # Track signal persistence - only entry if signal consistent for 2+ candles - # Fix 3: Use direction-only key (not exact price) and persist to file - signal_key = smc_signal.signal_type # "BUY" or "SELL" — direction matters, not exact price - current_time = time.time() - - if not hasattr(self, '_signal_persistence'): - self._signal_persistence = self._load_signal_persistence() - - # Cleanup: Remove entries older than 30 minutes (1800 seconds) - self._signal_persistence = { - k: v for k, v in self._signal_persistence.items() - if current_time - v[1] < 1800 - } - - if signal_key not in self._signal_persistence: - self._signal_persistence[signal_key] = (1, current_time) - self._save_signal_persistence() - logger.debug(f"Signal confirmation: {signal_key} seen 1st time - waiting") - return None # Wait for confirmation - else: - count, _ = self._signal_persistence[signal_key] - self._signal_persistence[signal_key] = (count + 1, current_time) - - # Require at least 2 consecutive confirmations (2 candles) - count, _ = self._signal_persistence[signal_key] - if count < 2: - self._save_signal_persistence() - logger.debug(f"Signal confirmation: {signal_key} count={count} - waiting") - return None - - # Signal confirmed! Reset counter - logger.info(f"Signal CONFIRMED: {signal_key} after {count} checks") - self._signal_persistence[signal_key] = (0, current_time) - self._save_signal_persistence() + # ML filters DISABLED — trading based on SMC only + # Signal persistence DISABLED — SMC signal = immediate trade # SMC-Only: Use SMC signal with confidence adjustment ml_agrees = ( @@ -2142,10 +2038,6 @@ class TradingBot: # Risk state risk_rec = self.smart_risk.get_trading_recommendation() - # News Agent status - news_can_trade, news_reason, _ = self.news_agent.should_trade() - news_status = "SAFE" if news_can_trade else "BLOCKED" - # Execution stats avg_exec = (sum(self._execution_times) / len(self._execution_times) * 1000) if self._execution_times else 0 uptime = (now - self._start_time).total_seconds() / 3600 # hours @@ -2180,9 +2072,9 @@ class TradingBot: uptime_hours=uptime, total_loops=self._loop_count, avg_execution_ms=avg_exec, - # News - news_status=news_status, - news_reason=news_reason, + # News - disabled + news_status="DISABLED", + news_reason="News agent disabled", ) self._last_hourly_report_time = now diff --git a/src/position_manager.py b/src/position_manager.py index 3acead0..248c23e 100644 --- a/src/position_manager.py +++ b/src/position_manager.py @@ -166,8 +166,9 @@ class SmartMarketCloseHandler: delta = target - now hours_to_weekend = delta.total_seconds() / 3600 - # Consider "near weekend" if within 12 hours of close (Friday afternoon WIB) - near_weekend = hours_to_weekend <= 12 and weekday == 4 # Friday only + # Consider "near weekend" if within 30 min of close (Saturday ~04:30 WIB) + # Market closes Saturday 05:00 WIB — Friday night trading is OK + near_weekend = hours_to_weekend <= 0.5 and weekday == 4 # Friday only return near_weekend, hours_to_weekend @@ -269,11 +270,15 @@ class SmartPositionManager: def __init__( self, - breakeven_pips: float = 15.0, # Move SL to breakeven after this profit - trail_start_pips: float = 25.0, # Start trailing after this profit - trail_step_pips: float = 10.0, # Trail by this amount + breakeven_pips: float = 15.0, # Fallback if ATR unavailable + trail_start_pips: float = 25.0, # Fallback if ATR unavailable + trail_step_pips: float = 10.0, # Fallback if ATR unavailable min_profit_to_protect: float = 50.0, # Minimum $ profit to protect max_drawdown_from_peak: float = 30.0, # Max % drawdown from peak profit + # ATR-adaptive exit multipliers (#24B: backtest +$373) + atr_be_mult: float = 2.0, # Breakeven = ATR * 2.0 + atr_trail_start_mult: float = 4.0, # Trail start = ATR * 4.0 + atr_trail_step_mult: float = 3.0, # Trail step = ATR * 3.0 # Market Close Handler settings enable_market_close_handler: bool = True, min_profit_before_close: float = 10.0, # Take profit if >= $10 near close @@ -282,6 +287,9 @@ class SmartPositionManager: self.breakeven_pips = breakeven_pips self.trail_start_pips = trail_start_pips self.trail_step_pips = trail_step_pips + self.atr_be_mult = atr_be_mult + self.atr_trail_start_mult = atr_trail_start_mult + self.atr_trail_step_mult = atr_trail_step_mult self.min_profit_to_protect = min_profit_to_protect self.max_drawdown_from_peak = max_drawdown_from_peak @@ -325,9 +333,16 @@ class SmartPositionManager: # Get market analysis market_analysis = self._analyze_market(df_market, regime_state, ml_prediction) + # Get current ATR for adaptive exit levels (#24B) + current_atr = None + if "atr" in df_market.columns: + atr_val = df_market["atr"].tail(1).item() + if atr_val is not None and atr_val > 0: + current_atr = atr_val + for row in positions.iter_rows(named=True): action = self._analyze_single_position( - row, market_analysis, current_price + row, market_analysis, current_price, current_atr ) if action: actions.append(action) @@ -421,6 +436,7 @@ class SmartPositionManager: pos: Dict, market: Dict, current_price: float, + current_atr: float = None, ) -> Optional[PositionAction]: """Analyze a single position and decide action.""" ticket = pos["ticket"] @@ -525,30 +541,41 @@ class SmartPositionManager: reason=f"High urgency exit (score: {market['urgency']}) - Securing ${profit:.2f}", ) - # === TRAILING STOP CONDITIONS === + # === TRAILING STOP CONDITIONS (ATR-adaptive #24B) === + + # Compute adaptive levels from ATR (fall back to fixed pips if ATR unavailable) + if current_atr is not None and current_atr > 0: + # ATR is in price terms; convert to pips (1 pip = 0.1 for gold) + be_pips = current_atr * self.atr_be_mult / 0.1 + trail_start = current_atr * self.atr_trail_start_mult / 0.1 + trail_step = current_atr * self.atr_trail_step_mult / 0.1 + else: + be_pips = self.breakeven_pips + trail_start = self.trail_start_pips + trail_step = self.trail_step_pips # 5. Breakeven protection - if pip_profit >= self.breakeven_pips and current_sl != 0: + if pip_profit >= be_pips and current_sl != 0: breakeven_sl = entry_price + (1 if is_buy else -1) * 2 # 2 points buffer if is_buy and current_sl < breakeven_sl: return PositionAction( ticket=ticket, action="TRAIL_SL", - reason=f"Moving SL to breakeven ({pip_profit:.1f} pips profit)", + reason=f"Moving SL to breakeven ({pip_profit:.1f}/{be_pips:.0f} pips)", new_sl=breakeven_sl, ) elif not is_buy and current_sl > breakeven_sl: return PositionAction( ticket=ticket, action="TRAIL_SL", - reason=f"Moving SL to breakeven ({pip_profit:.1f} pips profit)", + reason=f"Moving SL to breakeven ({pip_profit:.1f}/{be_pips:.0f} pips)", new_sl=breakeven_sl, ) - # 6. Trailing stop (after trail_start_pips) - if pip_profit >= self.trail_start_pips: - trail_distance = self.trail_step_pips * 0.1 # Convert to price + # 6. Trailing stop (after trail_start pips) + if pip_profit >= trail_start: + trail_distance = trail_step * 0.1 # Convert to price if is_buy: new_trail_sl = current_price - trail_distance @@ -556,7 +583,7 @@ class SmartPositionManager: return PositionAction( ticket=ticket, action="TRAIL_SL", - reason=f"Trailing SL ({pip_profit:.1f} pips profit)", + reason=f"Trailing SL ({pip_profit:.1f}/{trail_start:.0f} pips)", new_sl=new_trail_sl, ) else: @@ -565,7 +592,7 @@ class SmartPositionManager: return PositionAction( ticket=ticket, action="TRAIL_SL", - reason=f"Trailing SL ({pip_profit:.1f} pips profit)", + reason=f"Trailing SL ({pip_profit:.1f}/{trail_start:.0f} pips)", new_sl=new_trail_sl, ) diff --git a/src/session_filter.py b/src/session_filter.py index 8be5a35..c3a0898 100644 --- a/src/session_filter.py +++ b/src/session_filter.py @@ -104,8 +104,8 @@ class SessionFilter: start_hour=15, start_minute=0, end_hour=16, end_minute=0, volatility="high", - allow_trading=True, - position_size_multiplier=1.0, + allow_trading=False, # #24B: Skip Tokyo-London overlap (backtest +$345) + position_size_multiplier=0.0, ), TradingSession.OVERLAP_LONDON_NY: SessionConfig( name="London-NY Overlap (GOLDEN)", @@ -196,10 +196,11 @@ class SessionFilter: return False, "" def is_friday_close(self) -> bool: - """Check if approaching Friday market close.""" + """Check if approaching Friday market close (Saturday 05:00 WIB).""" now = self.get_current_time_wib() - # Friday = 4 (Monday=0) - if now.weekday() == 4 and now.hour >= 23: + # Market closes Saturday 05:00 WIB — only block 30 min before + # Saturday 04:30+ WIB + if now.weekday() == 5 and now.hour == 4 and now.minute >= 30: return True return False @@ -248,13 +249,13 @@ class SessionFilter: if not config.allow_trading: return False, f"Trading tidak diizinkan saat {config.name}", 0.0 - # In aggressive mode, allow high volatility + Sydney (proven profitable) + # In aggressive mode, allow medium+ volatility + Sydney (proven profitable) if self.aggressive_mode: # Sydney session is ALLOWED - backtest shows 62% WR, $5,934 profit if session == TradingSession.SYDNEY: return True, f"Trading OK - {config.name} (SAFE MODE: 0.5x lot)", config.position_size_multiplier - # Other low volatility sessions not allowed - if config.volatility not in ["high", "extreme"]: + # Only block low volatility sessions + if config.volatility not in ["medium", "high", "extreme"]: return False, f"Mode agresif: tunggu sesi {config.name} (volatilitas {config.volatility})", config.position_size_multiplier return True, f"Trading OK - {config.name} ({config.volatility} volatility)", config.position_size_multiplier diff --git a/src/smart_risk_manager.py b/src/smart_risk_manager.py index 49898fb..a09647d 100644 --- a/src/smart_risk_manager.py +++ b/src/smart_risk_manager.py @@ -690,7 +690,7 @@ class SmartRiskManager: loss_percent_of_max = abs(current_profit) / self.max_loss_per_trade * 100 # Cut early if momentum is against us AND loss is significant - if momentum < -30 and loss_percent_of_max >= 30: + if momentum < -50 and loss_percent_of_max >= 30: # #24B: relaxed from -30 (backtest +$125) logger.info(f"[EARLY CUT] Loss ${abs(current_profit):.2f} ({loss_percent_of_max:.0f}%) + weak momentum ({momentum:.0f}) - CUTTING EARLY") return True, ExitReason.TREND_REVERSAL, f"[EARLY CUT] Loss ${abs(current_profit):.2f} + momentum {momentum:.0f} - cutting to preserve daily limit" @@ -739,8 +739,12 @@ class SmartRiskManager: return True, ExitReason.DAILY_LIMIT, f"[LIMIT] Would exceed daily loss limit" # === CHECK 7: WEEKEND CLOSE === + # Market closes Saturday 05:00 WIB — only close 30 min before (Saturday 04:30 WIB) now = datetime.now(WIB) - if now.weekday() == 4 and now.hour >= 4: # Friday after 4 AM WIB + is_friday_late = now.weekday() == 4 and now.hour >= 4 and now.minute >= 30 # Sat 04:30 WIB = Fri weekday()==4 won't work + is_saturday_early = now.weekday() == 5 and now.hour < 5 # Saturday before 05:00 WIB + near_weekend_close = is_saturday_early and (now.hour >= 4 and now.minute >= 30) # Saturday 04:30+ WIB + if near_weekend_close: if current_profit > 0: return True, ExitReason.WEEKEND_CLOSE, f"[WEEKEND] Weekend close - profit ${current_profit:.2f}" elif current_profit > -10: