fix: critical improvements to trading logic and ML pipeline

main_live.py:
- Switch main loop from time-based (1s) to candle-based (M15)
- Add position-only checks between candles (every 10s)
- Fix memory leak in signal persistence dict (cleanup stale entries)
- Raise auto-retrain rollback AUC threshold from 0.52 to 0.60

src/ml_model.py:
- Add 50-bar gap between train/test split to prevent temporal leakage

src/smart_risk_manager.py:
- Remove dangerous "Smart Hold" behavior (holding losers waiting for golden time)
- Replace with proper early cut logic (loss >30% + negative momentum)

src/smc_polars.py:
- Fix lookahead bias in FVG detection (remove shift(-1), use confirmed bars only)
- Fix lookahead bias in Swing Points (use center=False rolling window)
- Fix lookahead bias in Order Blocks (validate with current bar, not future)
- Enforce minimum 1:2 Risk:Reward ratio on all signals
- Always use current_close as entry price (no stale FVG/OB zone prices)
- Add ATR sanity check with realistic XAUUSD default ($12)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
GifariKemal
2026-02-06 09:33:43 +07:00
co-authored by Claude Opus 4.6
parent 07e12f5229
commit 7eff3f1a2b
4 changed files with 377 additions and 204 deletions
+121 -31
View File
@@ -186,6 +186,8 @@ class TradingBot:
self._last_news_alert_reason: Optional[str] = None # Track news alert to avoid duplicates
self._current_session_multiplier: float = 1.0 # Session lot multiplier
self._is_sydney_session: bool = False # Sydney session flag (needs higher confidence)
self._last_candle_time: Optional[datetime] = None # Track last processed candle
self._position_check_interval: int = 10 # Check positions every N seconds between candles
def _load_models(self) -> bool:
"""Load pre-trained models."""
@@ -318,39 +320,111 @@ class TradingBot:
return [f for f in default_features if f in df.columns]
async def _main_loop(self):
"""Main trading loop."""
"""Main trading loop - CANDLE-BASED (not time-based)."""
last_position_check = time.time()
while self._running:
loop_start = time.perf_counter()
try:
# Check for new day
if date.today() != self._current_date:
self._on_new_day()
# Execute one loop iteration
await self._trading_iteration()
# Ensure MT5 connection is alive (auto-reconnect if needed)
if not self.mt5.ensure_connected():
logger.warning("MT5 disconnected, attempting reconnection...")
await asyncio.sleep(10) # Wait before retrying
continue
# Get current candle time to check if new candle formed
df_check = self.mt5.get_market_data(
symbol=self.config.symbol,
timeframe=self.config.execution_timeframe,
count=2,
)
if len(df_check) == 0:
logger.warning("No data received from MT5")
await asyncio.sleep(5)
continue
current_candle_time = df_check["time"].tail(1).item()
# Check if new candle formed
is_new_candle = (
self._last_candle_time is None or
current_candle_time > self._last_candle_time
)
if is_new_candle:
# NEW CANDLE: Run full analysis
self._last_candle_time = current_candle_time
await self._trading_iteration()
self._loop_count += 1
# Log on new candle
if self._loop_count % 4 == 0: # Every 4 candles (1 hour on M15)
avg_time = sum(self._execution_times[-4:]) / min(4, len(self._execution_times)) if self._execution_times else 0
logger.info(f"Candle #{self._loop_count} | Avg execution: {avg_time*1000:.1f}ms")
# AUTO-RETRAINING CHECK - every 20 candles (5 hours on M15)
if self._loop_count % 20 == 0:
await self._check_auto_retrain()
else:
# SAME CANDLE: Only check positions (every 10 seconds)
if time.time() - last_position_check >= self._position_check_interval:
await self._position_check_only()
last_position_check = time.time()
except Exception as e:
logger.error(f"Loop error: {e}")
import traceback
logger.debug(traceback.format_exc())
# Track execution time
execution_time = time.perf_counter() - loop_start
self._execution_times.append(execution_time)
# Log performance periodically
self._loop_count += 1
if self._loop_count % 60 == 0:
avg_time = sum(self._execution_times[-60:]) / min(60, len(self._execution_times))
logger.info(f"Loop #{self._loop_count} | Avg execution: {avg_time*1000:.1f}ms")
# AUTO-RETRAINING CHECK - every 5 minutes (300 loops)
if self._loop_count % 300 == 0:
await self._check_auto_retrain()
# Wait before next check (5 seconds between candle checks)
await asyncio.sleep(5)
# Wait for next iteration
await asyncio.sleep(1)
async def _position_check_only(self):
"""Quick position check without full analysis (between candles)."""
try:
open_positions = self.mt5.get_open_positions(
symbol=self.config.symbol,
magic=self.config.magic_number,
)
if len(open_positions) > 0 and not self.simulation:
# Get minimal data for position management
df = self.mt5.get_market_data(
symbol=self.config.symbol,
timeframe=self.config.execution_timeframe,
count=50, # Less data needed
)
if len(df) == 0:
return
# Calculate features for ML check
df = self.features.calculate_all(df, include_ml_features=True)
feature_cols = self._get_available_features(df)
ml_prediction = self.ml_model.predict(df, feature_cols)
tick = self.mt5.get_tick(self.config.symbol)
current_price = tick.bid if tick else df["close"].tail(1).item()
await self._smart_position_management(
open_positions=open_positions,
df=df,
regime_state=None,
ml_prediction=ml_prediction,
current_price=current_price,
)
except Exception as e:
logger.debug(f"Position check error: {e}")
async def _trading_iteration(self):
"""Single trading iteration."""
@@ -684,29 +758,44 @@ class TradingBot:
return None
# === IMPROVEMENT 2: Signal Confirmation (Entry Delay) ===
# Track signal persistence - only entry if signal consistent for 2+ loops
# Track signal persistence - only entry if signal consistent for 2+ candles
# FIX: Proper memory management to prevent leak
signal_key = f"{smc_signal.signal_type}_{smc_signal.entry_price:.0f}"
current_time = time.time()
if not hasattr(self, '_signal_persistence'):
self._signal_persistence = {}
self._signal_persistence = {} # {key: (count, last_seen_timestamp)}
# Cleanup: Remove entries older than 5 minutes (300 seconds)
# This prevents memory leak from accumulating stale signals
self._signal_persistence = {
k: v for k, v in self._signal_persistence.items()
if current_time - v[1] < 300 # Keep only signals seen in last 5 min
}
# Also limit to max 50 entries as safety
if len(self._signal_persistence) > 50:
# Keep only 20 most recent
sorted_signals = sorted(self._signal_persistence.items(), key=lambda x: x[1][1], reverse=True)
self._signal_persistence = dict(sorted_signals[:20])
if signal_key not in self._signal_persistence:
self._signal_persistence[signal_key] = 1
self._signal_persistence[signal_key] = (1, current_time)
logger.debug(f"Signal confirmation: {signal_key} seen 1st time - waiting")
# Clean old signals
self._signal_persistence = {k: v for k, v in self._signal_persistence.items()
if v < 10} # Keep only recent
return None # Wait for confirmation
else:
self._signal_persistence[signal_key] += 1
count, _ = self._signal_persistence[signal_key]
self._signal_persistence[signal_key] = (count + 1, current_time)
# Require at least 2 consecutive confirmations
if self._signal_persistence[signal_key] < 2:
logger.debug(f"Signal confirmation: {signal_key} count={self._signal_persistence[signal_key]} - waiting")
# Require at least 2 consecutive confirmations (2 candles)
count, _ = self._signal_persistence[signal_key]
if count < 2:
logger.debug(f"Signal confirmation: {signal_key} count={count} - waiting")
return None
# Signal confirmed! Reset counter
logger.info(f"Signal CONFIRMED: {signal_key} after {self._signal_persistence[signal_key]} checks")
self._signal_persistence[signal_key] = 0
logger.info(f"Signal CONFIRMED: {signal_key} after {count} checks")
self._signal_persistence[signal_key] = (0, current_time)
# SMC-Only: Use SMC signal with confidence adjustment
ml_agrees = (
@@ -1696,7 +1785,8 @@ class TradingBot:
logger.info(f" Test AUC: {results.get('xgb_test_auc', 0):.4f}")
# Check if new model is worse - rollback if needed
if results.get("xgb_test_auc", 0) < 0.52:
# FIX: Increased minimum AUC from 0.52 to 0.60 (0.52 is barely better than random)
if results.get("xgb_test_auc", 0) < 0.60:
logger.warning("New model AUC too low - rolling back!")
self.auto_trainer.rollback_models()
self.regime_detector.load()