print(f"[MOMENTUM] {pos['pair']}: Price ${current_price:.4f} vs Entry ${entry:.4f} ({price_change:.0%}) - REJECTING breakout check") return None direction = pos['direction'] entry = pos['entry_price'] tp1 = pos['take_profit1'] tp2 = pos['take_profit2'] # Additional validation: TP1 must be in correct direction from entry if direction == "LONG" and tp1 <= entry: print(f"[MOMENTUM] {pos['pair']}: Invalid TP1 (${tp1:.4f}) <= Entry (${entry:.4f}) for LONG") return None elif direction == "SHORT" and tp1 >= entry: print(f"[MOMENTUM] {pos['pair']}: Invalid TP1 (${tp1:.4f}) >= Entry (${entry:.4f}) for SHORT") return None if direction == "LONG": distance_to_tp1 = (tp1 - current_price) / current_price * 100 if current_price > tp1: # Calculate momentum but CAP at reasonable values momentum = (current_price - tp1) / tp1 * 100 # CAP: momentum cannot exceed 10% (avoid corruption) if momentum > 10.0: print(f"[MOMENTUM CAP] {pos['pair']}: Momentum {momentum:.1f}% capped to 10%") momentum = 10.0 if momentum > 0.5: # Calculate new TP2 with validation new_tp2 = current_price * 1.02 # Ensure TP2 is not absurd (max 2x from entry) if new_tp2 > entry * 2: print(f"[MOMENTUM CAP] {pos['pair']}: TP2 ${new_tp2:.4f} > 2x entry, capping") new_tp2 = entry * 1.5 # Cap at 50% gain return { 'action': 'EXTEND_TP', 'reason': f'Momentum breakout +{momentum:.1f}% beyond TP1', 'new_tp2': new_tp2, 'trail_activation': current_price * 0.995 } else: # SHORT distance_to_tp1 = (current_price - tp1) / current_price * 100 if current_price < tp1: momentum = (tp1 - current_price) / tp1 * 100 # CAP: momentum cannot exceed 10% if momentum > 10.0: print(f"[MOMENTUM CAP] {pos['pair']}: Momentum {momentum:.1f}% capped to 10%") momentum = 10.0 if momentum > 0.5: new_tp2 = current_price * 0.98 # Ensure TP2 is not absurd (min 0.5x from entry) if new_tp2 < entry * 0.5: print(f"[MOMENTUM CAP] {pos['pair']}: TP2 ${new_tp2:.4f} < 0.5x entry, capping") new_tp2 = entry * 0.5 # Cap at 50% loss return { 'action': 'EXTEND_TP', 'reason': f'Momentum breakout -{momentum:.1f}% below TP1', 'new_tp2': new_tp2, 'trail_activation': current_price * 1.005 } return None def consider_reverse_trade(self, invalidated_setup): pair = invalidated_setup.get('pair') original_direction = invalidated_setup.get('direction') try: klines = self.binance.get_klines(pair, '5m', 12) if not klines or len(klines) < 6: return None prices = [float(k[4]) for k in klines] volumes = [float(k[5]) for k in klines] recent_change = (prices[-1] - prices[-6]) / prices[-6] * 100 volume_surge = sum(volumes[-3:]) / (sum(volumes[-6:-3]) + 0.001) > 1.5 reverse_direction = "SHORT" if original_direction == "LONG" else "LONG" if original_direction == "LONG" and recent_change < -1.5 and volume_surge: self.log_activity(f"[b]REVERSE SIGNAL:[/b] {pair} LONG invalidated, strong SHORT momentum (-{recent_change:.1f}%)", "WARN") return {'pair': pair, 'direction': 'SHORT', 'reason': 'momentum_reversal'} elif original_direction == "SHORT" and recent_change > 1.5 and volume_surge: self.log_activity(f"[b]REVERSE SIGNAL:[/b] {pair} SHORT invalidated, strong LONG momentum (+{recent_change:.1f}%)", "WARN") return {'pair': pair, 'direction': 'LONG', 'reason': 'momentum_reversal'} except Exception as e: pass return None def monitor_positions(self): import time start_time = time.time() window_open = self.is_trading_window_open() if not hasattr(self, '_last_window_open'): self._last_window_open = window_open if self._last_window_open and not window_open: self.handle_trading_window_close() self._last_window_open = window_open for pos in self.position_manager.positions: if pos['status'] != 'OPEN': continue # Get current price with VALIDATION raw_current = self.market_data.get(pos['pair'], {}).get('price', pos['entry_price']) entry = pos['entry_price'] # VALIDATE: Price must be within 50% of entry (sanity check) if entry > 0 and raw_current > 0: price_change = abs(raw_current - entry) / entry if price_change > 0.5: # >50% change = likely corrupted print(f"[PRICE CORRUPTION] {pos['pair']}: Market data ${raw_current:.4f} vs Entry ${entry:.4f} ({price_change:.0%}), using entry") raw_current = entry # Fall back to entry price current = raw_current entry_distance = ((current - pos['entry_price']) / pos['entry_price']) * 100 if pos['direction'] == "SHORT": entry_distance = -entry_distance pos['entry_distance_pct'] = entry_distance # === PROFIT PROTECTION: Breakeven Stop === if getattr(self, 'profit_protection_enabled', True): entry = pos['entry_price'] direction = pos['direction'] sl = pos['stop_loss'] # Check if price moved enough to trigger breakeven trigger_pct = getattr(self, 'breakeven_trigger_pct', 1.0) profit_pct = getattr(self, 'breakeven_profit_pct', 0.3) if direction == "LONG": gain_pct = ((current - entry) / entry) * 100 # If moved trigger_pct in favor and SL is still below entry if gain_pct >= trigger_pct and sl < entry: new_sl = entry * (1 + profit_pct / 100) # Entry + small profit if new_sl > sl: # Only move if it's better pos['stop_loss'] = new_sl pos['sl_at_breakeven'] = True self.log_activity(f"[b]BREAKEVEN:[/b] {pos['pair']} SL moved to ${new_sl:.4f} (+{profit_pct}% profit)", "PROFIT") # LOG BREAKEVEN TRIGGER try: from __main__ import get_behavior_logger logger = get_behavior_logger() if logger: logger.log_protection_triggered(pos['pair'], "breakeven", { 'new_sl': new_sl, 'gain_pct': gain_pct, 'profit_pct': profit_pct }) except Exception as e: print(f"[BEHAVIOR LOG ERROR] {e}") else: # SHORT gain_pct = ((entry - current) / entry) * 100 if gain_pct >= trigger_pct and sl > entry: new_sl = entry * (1 - profit_pct / 100) # Entry - small profit if new_sl < sl: pos['stop_loss'] = new_sl pos['sl_at_breakeven'] = True self.log_activity(f"[b]BREAKEVEN:[/b] {pos['pair']} SL moved to ${new_sl:.4f} (+{profit_pct}% profit)", "PROFIT") # LOG BREAKEVEN TRIGGER try: from __main__ import get_behavior_logger logger = get_behavior_logger() if logger: logger.log_protection_triggered(pos['pair'], "breakeven", { 'new_sl': new_sl, 'gain_pct': gain_pct, 'profit_pct': profit_pct }) except Exception as e: print(f"[BEHAVIOR LOG ERROR] {e}") # === PROFIT PROTECTION: Time-based Exit === if getattr(self, 'profit_protection_enabled', True): import time entry_time = pos.get('entry_time', 0) time_exit_hours = getattr(self, 'time_exit_hours', 4) min_profit_pct = getattr(self, 'time_exit_min_profit', 0.5) if entry_time and (time.time() - entry_time) > (time_exit_hours * 3600): # Position been open too long, check if profitable if direction == "LONG": current_profit_pct = ((current - entry) / entry) * 100 else: current_profit_pct = ((entry - current) / entry) * 100 if current_profit_pct >= min_profit_pct: self.log_activity(f"[b]TIME EXIT:[/b] {pos['pair']} open {time_exit_hours}h+ with +{current_profit_pct:.2f}% profit - closing", "PROFIT") qty = pos['quantity'] # LOG TIME EXIT try: from __main__ import get_behavior_logger logger = get_behavior_logger() if logger: logger.log_protection_triggered(pos['pair'], "time_exit", { 'hours_open': time_exit_hours, 'profit_pct': current_profit_pct }) except Exception as e: print(f"[BEHAVIOR LOG ERROR] {e}") pnl = self.position_manager.close_position(pos['id'], current, 'TIME_EXIT_PROFIT', qty) self.log_activity(f" Time-based profit exit | PnL: ${pnl:+.2f}", "PROFIT") continue # Skip rest of processing for this position # === PROFIT PROTECTION: Dollar-Based Profit Lock === if getattr(self, 'dollar_profit_protection', True): min_profit = getattr(self, 'min_profit_dollars', 5.0) lock_trigger = getattr(self, 'profit_lock_trigger', 8.0) lock_amount = getattr(self, 'profit_lock_amount', 5.0) # Calculate current unrealized PnL in dollars qty = pos['quantity'] entry = pos['entry_price'] direction = pos['direction'] # Validate quantity to prevent division by zero if qty <= 0: continue if direction == "LONG": current_pnl = (current - entry) * qty else: current_pnl = (entry - current) * qty # Check if we've hit the $8 trigger and haven't locked yet if not pos.get('profit_locked', False): if current_pnl >= lock_trigger: # Calculate the price where we lock in $5 profit if direction == "LONG": lock_price = entry + (lock_amount / qty) # Only move SL if lock_price is better than current SL if lock_price > pos['stop_loss']: pos['stop_loss'] = lock_price pos['profit_locked'] = True pos['profit_locked_at'] = lock_amount self.log_activity(f"[b]PROFIT LOCK:[/b] {pos['pair']} at ${current_pnl:.2f} profit", "PROFIT") self.log_activity(f" SL moved to ${lock_price:.4f} (locking ${lock_amount} profit)", "PROFIT") # LOG PROFIT LOCK try: from __main__ import get_behavior_logger logger = get_behavior_logger() if logger: logger.log_protection_triggered(pos['pair'], "profit_lock", { 'current_pnl': current_pnl, 'locked_amount': lock_amount, 'lock_price': lock_price }) except Exception as e: print(f"[BEHAVIOR LOG ERROR] {e}") else: # SHORT lock_price = entry - (lock_amount / qty) if lock_price < pos['stop_loss']: pos['stop_loss'] = lock_price pos['profit_locked'] = True pos['profit_locked_at'] = lock_amount self.log_activity(f"[b]PROFIT LOCK:[/b] {pos['pair']} at ${current_pnl:.2f} profit", "PROFIT") self.log_activity(f" SL moved to ${lock_price:.4f} (locking ${lock_amount} profit)", "PROFIT") # LOG PROFIT LOCK try: from __main__ import get_behavior_logger logger = get_behavior_logger() if logger: logger.log_protection_triggered(pos['pair'], "profit_lock", { 'current_pnl': current_pnl, 'locked_amount': lock_amount, 'lock_price': lock_price }) except Exception as e: print(f"[BEHAVIOR LOG ERROR] {e}") if pos['direction'] == "LONG": tp1_distance = ((pos['take_profit1'] - current) / current) * 100 else: tp1_distance = ((current - pos['take_profit1']) / current) * 100 pos['tp1_distance_pct'] = max(0, tp1_distance) breakout = self.check_momentum_breakout(pos, current) if breakout and breakout['action'] == 'EXTEND_TP': old_tp2 = pos['take_profit2'] pos['take_profit2'] = breakout['new_tp2'] pos['dynamic_tp_active'] = True pos['trail_trigger_price'] = breakout['trail_activation'] self.log_activity(f"[b]DYNAMIC TP:[/b] {pos['pair']} {breakout['reason']}", "SETUP") self.log_activity(f" TP2 extended: ${old_tp2:.4f} → ${breakout['new_tp2']:.4f}", "SETUP") if pos.get('dynamic_tp_active') and not pos.get('trailing_stop'): if pos['direction'] == "LONG" and current >= pos.get('trail_trigger_price', 0): pos['trailing_stop'] = current * 0.995 self.log_activity(f"[b]TRAIL ACTIVATED:[/b] {pos['pair']} @ ${pos['trailing_stop']:.4f}", "PROFIT") elif pos['direction'] == "SHORT" and current <= pos.get('trail_trigger_price', float('inf')): pos['trailing_stop'] = current * 1.005 self.log_activity(f"[b]TRAIL ACTIVATED:[/b] {pos['pair']} @ ${pos['trailing_stop']:.4f}", "PROFIT") # SMART TRAILING: Use order book analysis if enabled and TP1 hit if self.use_smart_trailing and self.smart_trail_manager and pos.get('tp1_hit'): smart_trail = self.smart_trail_manager.update_trailing_stop(pos, current) if smart_trail: direction = pos['direction'] old_trail = pos.get('trailing_stop', 0) # Only update if smart trail is better (higher for long, lower for short) if direction == 'LONG' and smart_trail > old_trail: pos['trailing_stop'] = smart_trail # Log only when trail moves significantly if old_trail == 0 or abs(smart_trail - old_trail) / old_trail > 0.001: self.log_activity(f"[b]SMART TRAIL:[/b] {pos['pair']} moved to ${smart_trail:.4f} (order book)", "PROFIT") elif direction == 'SHORT' and smart_trail < old_trail: pos['trailing_stop'] = smart_trail if old_trail == 0 or abs(smart_trail - old_trail) / old_trail > 0.001: self.log_activity(f"[b]SMART TRAIL:[/b] {pos['pair']} moved to ${smart_trail:.4f} (order book)", "PROFIT") # === PROFIT PROTECTION: Partial Profit Taking === if getattr(self, 'profit_protection_enabled', True) and getattr(self, 'min_profit_partial', True): if not pos.get('partial_profit_taken') and not pos.get('tp1_hit'): min_profit_pct = getattr(self, 'min_profit_exit_pct', 2.0) if direction == "LONG": current_gain_pct = ((current - entry) / entry) * 100 else: current_gain_pct = ((entry - current) / entry) * 100 # If we've hit minimum profit threshold (e.g., 2%) but TP1 not yet hit if current_gain_pct >= min_profit_pct: tp1 = pos['take_profit1'] tp1_distance_pct = abs(tp1 - current) / entry * 100 # If TP1 is still far away (more than 2x our current gain), take partial if tp1_distance_pct > min_profit_pct * 2: partial_qty = pos['quantity'] * 0.5 # Close 50% pnl = self.position_manager.close_position(pos['id'], current, 'PARTIAL_PROFIT', partial_qty) pos['partial_profit_taken'] = True pos['quantity'] -= partial_qty pos['remaining_qty'] = pos.get('remaining_qty', pos['quantity']) - partial_qty self.log_activity(f"[b]PARTIAL PROFIT:[/b] {pos['pair']} closed 50% at +{current_gain_pct:.2f}% (TP1 still {tp1_distance_pct:.1f}% away)", "PROFIT") self.log_activity(f" Locked in ${pnl:+.2f}, holding remaining {pos['quantity']:.2f} for TP1", "PROFIT") # === REVERSAL EXIT CHECK === # Check for timeframe reversal every 30 seconds (not on every tick) import time current_time = time.time() last_check = pos.get('last_reversal_check', 0) cooldown_until = pos.get('reversal_exits_blocked_until', 0) # FIX: Handle None values for manual trades last_check = last_check if last_check is not None else 0 cooldown_until = cooldown_until if cooldown_until is not None else 0 should_check_reversal = (current_time - last_check) > 30 and current_time > cooldown_until if should_check_reversal and not pos.get('tp1_hit', False): # Only check before TP1 pos['last_reversal_check'] = current_time pos['reversal_check_count'] = pos.get('reversal_check_count', 0) + 1 # Fetch current multi-timeframe data for this pair try: current_tf_data = self.binance.fetch_multi_timeframe_data(pos['pair']) # Check for reversal exit reversal_check = check_timeframe_reversal_exit(pos, current_tf_data, min_profit_pct=0.3) # Log detailed output for log_line in reversal_check['detailed_log']: print(log_line) if reversal_check['should_exit']: urgency = reversal_check['exit_urgency'] reason = reversal_check['exit_reason'] pnl_pct = reversal_check['pnl_pct'] if urgency == 'IMMEDIATE': self.log_activity(f"[b]🚨 REVERSAL EXIT:[/b] {pos['pair']} - {reason}", "PROFIT") self.log_activity(f" Taking {pnl_pct:.2f}% profit before reversal hits!", "PROFIT") # Close full position qty = pos['quantity'] pnl = self.position_manager.close_position(pos['id'], current, 'REVERSAL_EXIT', qty) self.log_activity(f" Closed by reversal exit | PnL: ${pnl:+.2f}", "PROFIT") self.backtest_logger.update_trade_outcome( pair=pos['pair'], entry_price=pos['entry_price'], exit_price=current, pnl=pnl, exit_reason='REVERSAL_EXIT', direction=pos['direction'] ) # Update Market Brain with trade outcome for learning if hasattr(self, 'market_brain') and self.market_brain: pnl_pct_calc = (pnl / (pos['entry_price'] * pos['quantity'])) * 100 if pos['quantity'] > 0 else 0 self.market_brain.update_trade_outcome( result='WIN' if pnl > 0 else 'LOSS', pnl_pct=pnl_pct_calc, grade=pos.get('entry_weighted_grade', 'C'), setup_type=pos.get('setup_type', 'UNKNOWN'), trade_pattern=pos.get('trade_pattern', 'standard') ) stats = self.position_manager.get_stats() self.circuit_breaker.update(stats.get('total_pnl', 0), {'pnl': pnl, 'closed': True}) # Flag for potential re-entry in opposite direction opposite_dir = 'SHORT' if pos['direction'] == 'LONG' else 'LONG' self.log_activity(f"[b]REVERSAL FLAG:[/b] {pos['pair']} - Watch for {opposite_dir} re-entry", "SETUP") continue # Skip remaining checks for this position elif urgency == 'CAUTION': # Just warn for now, don't auto-exit on caution self.log_activity(f"[b]⚠️ REVERSAL WARNING:[/b] {pos['pair']} - {reason}", "INFO") self.log_activity(f" Current profit: {pnl_pct:.2f}% - Monitor closely!", "INFO") except Exception as e: print(f"[REVERSAL_CHECK] Error checking {pos['pair']}: {e}") # === SCALP EXIT CHECK === # Check for scalp-specific exits if this is a scalp trade if pos.get('is_scalp_trade', False) and pos['status'] == 'OPEN': try: current_tf_data = self.binance.fetch_multi_timeframe_data(pos['pair']) entry_time = pos.get('scalp_entry_time', time.time()) scalp_exit = check_scalp_exit(pos, current_tf_data, dxy_change=0, entry_time=entry_time, current_time=time.time()) # Log scalp exit check details for log_line in scalp_exit['detailed_log']: print(log_line) if scalp_exit['should_exit']: exit_type = scalp_exit['exit_type'] exit_action = scalp_exit['exit_action'] reason = scalp_exit['reason'] pnl_pct = scalp_exit['pnl_pct'] if exit_type == 'EMERGENCY': self.log_activity(f"[b]🚨 SCALP EMERGENCY EXIT:[/b] {pos['pair']}", "LOSS") self.log_activity(f" {reason}", "LOSS") qty = pos['quantity'] pnl = self.position_manager.close_position(pos['id'], current, 'SCALP_EMERGENCY', qty) self.log_activity(f" Closed all | PnL: ${pnl:+.2f}", "LOSS") elif exit_action == 'CLOSE_50': self.log_activity(f"[b]⚡ SCALP PROFIT EXIT:[/b] {pos['pair']}", "PROFIT") self.log_activity(f" {reason}", "PROFIT") self.log_activity(f" Taking TP1 early at {pnl_pct:.2f}%", "PROFIT") # Close 50% (TP1 quantity) qty = pos.get('tp1_quantity', pos['quantity'] * 0.5) pnl = self.position_manager.close_position(pos['id'], current, 'SCALP_TP1_EARLY', qty) pos['tp1_hit'] = True pos['sl_at_breakeven'] = True self.log_activity(f" Closed 50% | PnL: ${pnl:+.2f}", "PROFIT") elif exit_action == 'CLOSE_ALL': self.log_activity(f"[b]⚡ SCALP FULL EXIT:[/b] {pos['pair']}", "PROFIT") self.log_activity(f" {reason}", "PROFIT") qty = pos['quantity'] pnl = self.position_manager.close_position(pos['id'], current, 'SCALP_FULL_EXIT', qty) self.log_activity(f" Closed all | PnL: ${pnl:+.2f}", "PROFIT") elif exit_action == 'CLOSE_RUNNER': self.log_activity(f"[b]⚡ SCALP RUNNER EXIT:[/b] {pos['pair']}", "PROFIT") self.log_activity(f" {reason}", "PROFIT") # Close remaining 25% qty = pos.get('tp2_quantity', pos['quantity'] * 0.25) pnl = self.position_manager.close_position(pos['id'], current, 'SCALP_RUNNER_EXIT', qty) pos['tp2_hit'] = True self.log_activity(f" Closed runner | PnL: ${pnl:+.2f}", "PROFIT") elif exit_action == 'MOVE_SL_BREAKEVEN': # Move SL to breakeven + 0.05% if not pos.get('sl_at_breakeven', False): if pos['direction'] == 'LONG': new_sl = pos['entry_price'] * 1.0005 else: new_sl = pos['entry_price'] * 0.9995 pos['stop_loss'] = new_sl pos['sl_at_breakeven'] = True self.log_activity(f"[b]⚡ SCALP TRAIL:[/b] {pos['pair']} SL moved to breakeven+ (${new_sl:.2f})", "PROFIT") # Update backtest and circuit breaker if position closed if exit_action in ['CLOSE_ALL', 'EMERGENCY']: self.backtest_logger.update_trade_outcome( pair=pos['pair'], entry_price=pos['entry_price'], exit_price=current, pnl=pnl, exit_reason=f'SCALP_{exit_type}', direction=pos['direction'] ) # Update Market Brain with trade outcome for learning if hasattr(self, 'market_brain') and self.market_brain: pnl_pct_calc = (pnl / (pos['entry_price'] * pos['quantity'])) * 100 if pos['quantity'] > 0 else 0 self.market_brain.update_trade_outcome( result='WIN' if pnl > 0 else 'LOSS', pnl_pct=pnl_pct_calc, grade=pos.get('entry_weighted_grade', 'C'), setup_type=pos.get('setup_type', 'UNKNOWN'), trade_pattern=pos.get('trade_pattern', 'standard') ) stats = self.position_manager.get_stats() self.circuit_breaker.update(stats.get('total_pnl', 0), {'pnl': pnl, 'closed': True}) continue elif exit_action in ['CLOSE_50', 'CLOSE_RUNNER']: self.backtest_logger.update_trade_outcome( pair=pos['pair'], entry_price=pos['entry_price'], exit_price=current, pnl=pnl, exit_reason=f'SCALP_{exit_action}', direction=pos['direction'] ) # Update Market Brain with trade outcome for learning (partial exit still counts) if hasattr(self, 'market_brain') and self.market_brain: pnl_pct_calc = (pnl / (pos['entry_price'] * qty)) * 100 if qty > 0 else 0 self.market_brain.update_trade_outcome( result='WIN' if pnl > 0 else 'LOSS', pnl_pct=pnl_pct_calc, grade=pos.get('entry_weighted_grade', 'C'), setup_type=pos.get('setup_type', 'UNKNOWN'), trade_pattern=pos.get('trade_pattern', 'standard') ) except Exception as e: print(f"[SCALP_EXIT] Error checking {pos['pair']}: {e}") import traceback traceback.print_exc() # === EXISTING EXIT LOGIC === result, qty = self.position_manager.update_position(pos['id'], current, self.binance) if result == 'TP1_HIT': self.log_activity(f"[b]TP1 HIT:[/b] {pos['pair']} - Moved SL to breakeven+", "PROFIT") pnl = self.position_manager.close_position(pos['id'], current, 'TP1', qty) self.log_activity(f" Closed 50% @ TP1 | PnL: ${pnl:+.2f}", "PROFIT") self.backtest_logger.update_trade_outcome( pair=pos['pair'], entry_price=pos['entry_price'], exit_price=current, pnl=pnl, exit_reason='TP1', direction=pos['direction'] ) # Update Market Brain with trade outcome for learning if hasattr(self, 'market_brain') and self.market_brain: pnl_pct = (pnl / (pos['entry_price'] * pos['quantity'])) * 100 if pos['quantity'] > 0 else 0 self.market_brain.update_trade_outcome( result='WIN' if pnl > 0 else 'LOSS', pnl_pct=pnl_pct, grade=pos.get('entry_weighted_grade', 'C'), setup_type=pos.get('setup_type', 'UNKNOWN'), trade_pattern=pos.get('trade_pattern', 'standard') ) stats = self.position_manager.get_stats() self.circuit_breaker.update(stats.get('total_pnl', 0), {'pnl': pnl, 'closed': True}) elif result == 'TP2_HIT': self.log_activity(f"[b]TP2 HIT:[/b] {pos['pair']} - Full target reached!", "PROFIT") pnl = self.position_manager.close_position(pos['id'], current, 'TP2', qty) self.log_activity(f" Closed remaining @ TP2 | PnL: ${pnl:+.2f}", "PROFIT") self.backtest_logger.update_trade_outcome( pair=pos['pair'], entry_price=pos['entry_price'], exit_price=current, pnl=pnl, exit_reason='TP2', direction=pos['direction'] ) # Update Market Brain with trade outcome for learning if hasattr(self, 'market_brain') and self.market_brain: pnl_pct = (pnl / (pos['entry_price'] * pos['quantity'])) * 100 if pos['quantity'] > 0 else 0 self.market_brain.update_trade_outcome( result='WIN' if pnl > 0 else 'LOSS', pnl_pct=pnl_pct, grade=pos.get('entry_weighted_grade', 'C'), setup_type=pos.get('setup_type', 'UNKNOWN'), trade_pattern=pos.get('trade_pattern', 'standard') ) stats = self.position_manager.get_stats() self.circuit_breaker.update(stats.get('total_pnl', 0), {'pnl': pnl, 'closed': True}) elif result == 'TRAILING_STOP': self.log_activity(f"[b]TRAILING STOP:[/b] {pos['pair']}", "PROFIT") qty = pos['quantity'] pnl = self.position_manager.close_position(pos['id'], current, 'TRAILING_STOP', qty) self.log_activity(f" Closed by trailing stop | PnL: ${pnl:+.2f}", "PROFIT") self.backtest_logger.update_trade_outcome( pair=pos['pair'], entry_price=pos['entry_price'], exit_price=current, pnl=pnl, exit_reason='TRAILING_STOP', direction=pos['direction'] ) # Update Market Brain with trade outcome for learning if hasattr(self, 'market_brain') and self.market_brain: pnl_pct = (pnl / (pos['entry_price'] * pos['quantity'])) * 100 if pos['quantity'] > 0 else 0 self.market_brain.update_trade_outcome( result='WIN' if pnl > 0 else 'LOSS', pnl_pct=pnl_pct, grade=pos.get('entry_weighted_grade', 'C'), setup_type=pos.get('setup_type', 'UNKNOWN'), trade_pattern=pos.get('trade_pattern', 'standard') ) stats = self.position_manager.get_stats() self.circuit_breaker.update(stats.get('total_pnl', 0), {'pnl': pnl, 'closed': True}) elif result == 'SL_HIT': self.log_activity(f"[b]STOP LOSS:[/b] {pos['pair']}", "LOSS") qty = pos['quantity'] pnl = self.position_manager.close_position(pos['id'], current, 'STOP_LOSS', qty) self.log_activity(f" Stopped out | PnL: ${pnl:+.2f}", "LOSS") self.backtest_logger.update_trade_outcome( pair=pos['pair'], entry_price=pos['entry_price'], exit_price=current, pnl=pnl, exit_reason='STOP_LOSS', direction=pos['direction'] ) # Update Market Brain with trade outcome for learning if hasattr(self, 'market_brain') and self.market_brain: pnl_pct = (pnl / (pos['entry_price'] * pos['quantity'])) * 100 if pos['quantity'] > 0 else 0 self.market_brain.update_trade_outcome( result='LOSS', pnl_pct=pnl_pct, grade=pos.get('entry_weighted_grade', 'C'), setup_type=pos.get('setup_type', 'UNKNOWN'), trade_pattern=pos.get('trade_pattern', 'standard') ) stats = self.position_manager.get_stats() self.circuit_breaker.update(stats.get('total_pnl', 0), {'pnl': pnl, 'closed': True}) elif result == 'BREAKEVEN_SL': self.log_activity(f"[b]BREAKEVEN STOP:[/b] {pos['pair']} - Protected after TP1", "INFO") qty = pos['tp2_quantity'] pnl = self.position_manager.close_position(pos['id'], current, 'BREAKEVEN_SL', qty) self.log_activity(f" Exited at breakeven | PnL: ${pnl:+.2f}", "INFO") self.backtest_logger.update_trade_outcome( pair=pos['pair'], entry_price=pos['entry_price'], exit_price=current, pnl=pnl, exit_reason='BREAKEVEN_SL', direction=pos['direction'] ) # Update Market Brain with trade outcome for learning if hasattr(self, 'market_brain') and self.market_brain: pnl_pct = (pnl / (pos['entry_price'] * pos['quantity'])) * 100 if pos['quantity'] > 0 else 0 self.market_brain.update_trade_outcome( result='WIN' if pnl > 0 else 'LOSS', pnl_pct=pnl_pct, grade=pos.get('entry_weighted_grade', 'C'), setup_type=pos.get('setup_type', 'UNKNOWN'), trade_pattern=pos.get('trade_pattern', 'standard') ) stats = self.position_manager.get_stats() self.circuit_breaker.update(stats.get('total_pnl', 0), {'pnl': pnl, 'closed': True}) elif result == 'ULTRA_TRAIL_EXIT': self.log_activity(f"[b]ULTRA TRAIL:[/b] {pos['pair']} - Tight 0.15% trail hit!", "PROFIT") qty = pos.get('remaining_qty', pos['quantity'] * 0.2) pnl = self.position_manager.close_position(pos['id'], current, 'ULTRA_TRAIL', qty) self.log_activity(f" Final exit by ultra-tight trail | PnL: ${pnl:+.2f}", "PROFIT") self.backtest_logger.update_trade_outcome( pair=pos['pair'], entry_price=pos['entry_price'], exit_price=current, pnl=pnl, exit_reason='ULTRA_TRAIL', direction=pos['direction'] ) # Update Market Brain with trade outcome for learning if hasattr(self, 'market_brain') and self.market_brain: pnl_pct = (pnl / (pos['entry_price'] * pos['quantity'])) * 100 if pos['quantity'] > 0 else 0 self.market_brain.update_trade_outcome( result='WIN' if pnl > 0 else 'LOSS', pnl_pct=pnl_pct, grade=pos.get('entry_weighted_grade', 'C'), setup_type=pos.get('setup_type', 'UNKNOWN'), trade_pattern=pos.get('trade_pattern', 'standard') ) stats = self.position_manager.get_stats() self.circuit_breaker.update(stats.get('total_pnl', 0), {'pnl': pnl, 'closed': True}) elif result == 'TIME_EXIT': self.log_activity(f"[b]TIME EXIT:[/b] {pos['pair']} - Closing after 4h", "WARN") pnl = self.position_manager.close_position(pos['id'], current, 'TIME_EXIT', qty) self.log_activity(f" Closed remaining @ ${current:.2f} | PnL: ${pnl:+.2f}", "INFO") self.backtest_logger.update_trade_outcome( pair=pos['pair'], entry_price=pos['entry_price'], exit_price=current, pnl=pnl, exit_reason='TIME_EXIT', direction=pos['direction'] ) # Update Market Brain with trade outcome for learning if hasattr(self, 'market_brain') and self.market_brain: pnl_pct = (pnl / (pos['entry_price'] * pos['quantity'])) * 100 if pos['quantity'] > 0 else 0 self.market_brain.update_trade_outcome( result='WIN' if pnl > 0 else 'LOSS', pnl_pct=pnl_pct, grade=pos.get('entry_weighted_grade', 'C'), setup_type=pos.get('setup_type', 'UNKNOWN'), trade_pattern=pos.get('trade_pattern', 'standard') ) stats = self.position_manager.get_stats() self.circuit_breaker.update(stats.get('total_pnl', 0), {'pnl': pnl, 'closed': True}) self._check_pause_state() # Track position update time for TEST tab metrics import time elapsed_ms = (time.time() - start_time) * 1000 self.position_manager._last_update_time_ms = elapsed_ms def force_market_scan(self, immediate_entry=False): # Reset scan completion flag - new scan is starting self._scan_completed = False self._scan_start_time = time.time() # Track when scan started for emergency timeout print(f"[SCAN START] _scan_start_time set to {self._scan_start_time}") # UI updates must be on main thread def update_ui_start(dt): self.setups_status_lbl.text = "SCANNING..." self.setups_status_lbl.color = AMBER self.analyzing_lbl.text = "SCANNING..." Clock.schedule_once(update_ui_start, 0) with self._filter_lock: limits_off = self.all_limits_off pairs_to_scan = ['BTCUSDC'] if DEBUG_BTC_ONLY else self.get_scan_pairs() # CHILL MODE: If positions are full, only scan top 10 by volume/market cap open_positions = [p for p in getattr(self, 'positions', []) if p.get('status') == 'OPEN'] max_positions = getattr(self, 'max_simultaneous', 2) if len(open_positions) >= max_positions: # Sort by trading priority (BTC/ETH first, then by volume) def pair_priority(pair): # Priority: BTC first, ETH second, then by volume estimate if 'BTC' in pair: return (0, 0) elif 'ETH' in pair: return (1, 0) else: # Get approximate volume ranking from coin_data if available base = pair.replace('USDC', '').replace('USDT', '') volume_rank = self.coin_data.get(base, {}).get('volume_24h_rank', 999) return (2, volume_rank) pairs_to_scan = sorted(pairs_to_scan, key=pair_priority)[:10] self.log_activity(f"[CHILL MODE] Positions full ({len(open_positions)}/{max_positions}) - scanning TOP 10 only", "INFO") if hasattr(self, 'behavior_logger'): self.behavior_logger.log_behavior("SCAN", "chill_mode_top_10", { "open_positions": len(open_positions), "max_positions": max_positions, "pairs_scanned": pairs_to_scan }) else: self.log_activity(f"[SCAN] {len(open_positions)}/{max_positions} positions - scanning {len(pairs_to_scan)} pairs", "INFO") # DEBUG: Verify what we got print(f"[SCAN START DEBUG] Raw pairs_to_scan: {pairs_to_scan[:15]}...") print(f"[SCAN START DEBUG] Total pairs: {len(pairs_to_scan)}") usdc_in_scan = len([p for p in pairs_to_scan if 'USDC' in p]) usdt_in_scan = len([p for p in pairs_to_scan if 'USDT' in p]) print(f"[SCAN START] pairs_to_scan: {len(pairs_to_scan)} total, {usdc_in_scan} USDC, {usdt_in_scan} USDT") print(f"[SCAN START] First 10 pairs: {pairs_to_scan[:10]}") if self.test_mode or limits_off: mode_str = "TEST MODE (all grades)" elif self.min_grade_filter == "F" or self.filter_mode == "OFF": mode_str = "ALL GRADES - Brain selects best" else: mode_str = f"FILTERED (min grade: {self.min_grade_filter})" self.log_activity(f"[DEBUG] Starting scan - {mode_str}") self.log_activity(f"[DEBUG] Scanning {len(pairs_to_scan)} pairs: {', '.join(pairs_to_scan[:5])}...") def scan_pair(pair): """Scan a single pair with timeout protection.""" import time start_time = time.time() # CHECK CACHE FIRST - avoid redundant calculations cache_entry = self._setup_cache.get(pair) if cache_entry: age = time.time() - cache_entry['timestamp'] if age < self._setup_cache_ttl: # Cache hit - return cached result return { 'pair': pair, 'setup': cache_entry['setup'], 'market_data': cache_entry['market_data'], 'log': f"[DEBUG] {pair}: CACHE HIT (age={age:.1f}s)", 'multi_tf': cache_entry.get('multi_tf', False), 'cached': True } try: # Try multi-timeframe scan first with timeout tf_data = self.binance.fetch_multi_timeframe_data(pair) # Check if we've exceeded time budget for this pair elapsed = time.time() - start_time if elapsed > 3.0: # Max 3 seconds per pair print(f"[SCAN TIMEOUT] {pair}: Multi-TF took {elapsed:.1f}s, using fallback") raise TimeoutError(f"Multi-TF scan timeout: {elapsed:.1f}s") # Require ALL 6 timeframes to be fetched before trading all_timeframes = ['3m', '5m', '15m', '1h', '2h', '4h'] has_all_timeframes = all(tf_data.get(tf) and tf_data[tf].get('prices') and len(tf_data[tf]['prices']) >= 30 for tf in all_timeframes) if has_all_timeframes: with self._filter_lock: all_limits_val2 = self.all_limits_off # Calculate exhaustion score for reversal detection prices_15m = tf_data['15m']['prices'] volumes_15m = tf_data['15m']['volumes'] exhaustion = calculate_momentum_exhaustion(prices_15m, volumes_15m) exhaustion_score = exhaustion.get('score', 50) setup = scan_multi_timeframe( tf_data, pair, no_filter=(self.test_mode or all_limits_val2), min_grade=self.min_grade_filter, scalping_mode=getattr(self, 'scalping_mode_enabled', False), scalp_target_pct=getattr(self, 'scalping_profit_target', 0.004), scalp_stop_pct=getattr(self, 'scalping_stop_loss', 0.003), lower_tf_priority=getattr(self, 'lower_tf_priority', False), min_consensus=getattr(self, 'min_consensus', 4), exhaustion_score=exhaustion_score ) if setup and setup.get('detected'): current_price = tf_data['15m']['prices'][-1] market_data = { 'price': current_price, 'volume_24h': sum(tf_data['1h']['volumes'][-24:]) if len(tf_data['1h']['volumes']) >= 24 else sum(tf_data['1h']['volumes']), 'change_24h': ((tf_data['1h']['prices'][-1] - tf_data['1h']['prices'][-24]) / tf_data['1h']['prices'][-24] * 100) if len(tf_data['1h']['prices']) >= 24 else 0 } # === SCALP DETECTION === # Check for scalp opportunity using 2m/3m timeframe data scalp_result = detect_scalp_setup(tf_data, pair, dxy_change=0, fear_greed_index=50) # Print scalp detection logs for log_line in scalp_result['detailed_log']: print(log_line) if scalp_result['is_scalp']: setup['is_scalp'] = True setup['scalp_confidence'] = scalp_result['confidence'] setup['scalp_direction'] = scalp_result['direction'] setup['scalp_score'] = scalp_result['score'] setup['scalp_confirmations'] = scalp_result['confirmations'] self.log_activity(f"[b]⚡ SCALP DETECTED:[/b] {pair} {scalp_result['direction']} {scalp_result['confidence']}% confidence", "SETUP") else: setup['is_scalp'] = False setup['scalp_confidence'] = 0 # Log to behavior logger if hasattr(self, 'behavior_logger'): self.behavior_logger.log_behavior("SETUP", "setup_detected", { "pair": pair, "direction": setup.get('direction'), "grade": setup.get('grade'), "score": setup.get('confluence_score'), "is_scalp": setup.get('is_scalp', False) }) return { 'pair': pair, 'setup': setup, 'market_data': market_data, 'log': f"[DEBUG] {pair}: MULTI-TF SETUP - C-Score {setup.get('confluence_score', 0)} Grade {setup.get('grade', 'N/A')}", 'multi_tf': True } # Fallback to single timeframe klines = self.binance.get_klines(pair, '1h', 50) elapsed = time.time() - start_time if elapsed > 5.0: # Hard limit 5 seconds per pair return {'pair': pair, 'setup': None, 'market_data': None, 'log': f"[DEBUG] {pair}: TIMEOUT after {elapsed:.1f}s"} if not klines: return {'pair': pair, 'setup': None, 'market_data': None, 'log': f"[DEBUG] No klines for {pair}"} if not isinstance(klines, list) or len(klines) == 0: return {'pair': pair, 'setup': None, 'market_data': None, 'log': f"[DEBUG] Invalid klines for {pair}"} if not isinstance(klines[0], (list, tuple)) or len(klines[0]) < 6: return {'pair': pair, 'setup': None, 'market_data': None, 'log': f"[DEBUG] Invalid candle format for {pair}"} prices = [float(k[4]) for k in klines if isinstance(k, (list, tuple)) and len(k) > 4] volumes = [float(k[5]) for k in klines if isinstance(k, (list, tuple)) and len(k) > 5] if len(prices) < 10 or len(volumes) < 10: return {'pair': pair, 'setup': None, 'market_data': None, 'log': f"[DEBUG] Insufficient data for {pair}"} market_data = None if prices: current_price = prices[-1] market_data = { 'price': current_price, 'volume_24h': sum(volumes[-24:]) if len(volumes) >= 24 else sum(volumes), 'change_24h': ((prices[-1] - prices[-24]) / prices[-24] * 100) if len(prices) >= 24 else 0 } with self._filter_lock: all_limits_val2 = self.all_limits_off setup = scan_setups(prices, volumes, pair, no_filter=(self.test_mode or all_limits_val2), min_grade=self.min_grade_filter, scalping_mode=getattr(self, 'scalping_mode_enabled', False), scalp_target_pct=getattr(self, 'scalping_profit_target', 0.004), scalp_stop_pct=getattr(self, 'scalping_stop_loss', 0.003)) if setup is None: return { 'pair': pair, 'setup': None, 'market_data': market_data, 'log': f"[DEBUG] {pair}: scan_setups returned None (prices < 30 or no direction)" } if setup.get('detected'): return { 'pair': pair, 'setup': setup, 'market_data': market_data, 'log': f"[DEBUG] {pair}: SETUP FOUND - Grade {setup.get('grade', 'N/A')} Score {setup.get('score', 0)} R/R {setup.get('rr_ratio', 0):.1f}", 'multi_tf': False } else: reasons = ', '.join(setup.get('rejection_reasons', ['Unknown'])) return { 'pair': pair, 'setup': None, 'market_data': market_data, 'log': f"[DEBUG] {pair}: FILTERED - Grade {setup.get('grade', 'N/A')} Score {setup.get('score', 0)} - {reasons}", 'multi_tf': False } except Exception as e: import traceback error_detail = traceback.format_exc() print(f"[ERROR] {pair}: {e}\n{error_detail}") return {'pair': pair, 'setup': None, 'market_data': None, 'log': f"[DEBUG] Scan error {pair}: {type(e).__name__}: {e}"} def scan_worker(): all_setups = [] market_data_all = {} debug_logs = [] # PERFORMANCE TRACKING scan_start_time = time.time() pairs_scanned = 0 # AGGRESSIVE SCANNING: 30 pairs per tier for maximum coverage TIER_SIZES = [30, 30, 20, 20] # 100 pairs total in aggressive mode total_pairs = len(pairs_to_scan) tiers = [] start = 0 for size in TIER_SIZES: if start < total_pairs: end = min(start + size, total_pairs) tiers.append(pairs_to_scan[start:end]) start = end if start < total_pairs: tiers.append(pairs_to_scan[start:]) # DEBUG: Log what's in each tier for i, tier in enumerate(tiers, 1): usdc_count = len([p for p in tier if 'USDC' in p]) usdt_count = len([p for p in tier if 'USDT' in p]) self.log_activity(f"[SCAN TIER {i}] {len(tier)} pairs: {usdc_count} USDC, {usdt_count} USDT - First 5: {tier[:5]}") self.log_activity(f"[SCAN] Smart tiered scan: {total_pairs} pairs in {len(tiers)} tier(s)") best_grade_found = None grade_priority = {'S': 10, 'A+': 9, 'A': 8, 'A-': 7, 'B+': 6, 'B': 5, 'B-': 4, 'C': 3, 'D': 2, 'NO_TRADE': 1, None: 0} # Use existing thread pool instead of creating new one executor = self._thread_pool for tier_num, tier in enumerate(tiers, 1): # Check global timeout at start of each tier elapsed_total = time.time() - scan_start_time if elapsed_total > 60.0: # 60 second global timeout self.log_activity(f"[SCAN TIMEOUT] Scan exceeded 60s limit. Stopping at tier {tier_num}.", "WARN") debug_logs.append(f"[DEBUG] === SCAN TIMEOUT after {elapsed_total:.1f}s ===") break tier_name = {1: "MEGA CAPS", 2: "MAJOR ALTS", 3: "MID CAPS", 4: "EMERGING"}.get(tier_num, f"TIER {tier_num}") self.log_activity(f"[SCAN] {tier_name} (Tier {tier_num}/{len(tiers)}): {len(tier)} pairs...") from kivy.clock import Clock Clock.schedule_once(lambda dt, t=tier_name: setattr(self.analyzing_lbl, 'text', f"Scanning {t}..."), 0) # Submit all pairs in tier to existing executor future_to_pair = {executor.submit(scan_pair, pair): pair for pair in tier} for future in as_completed(future_to_pair): # Check global timeout elapsed_total = time.time() - scan_start_time if elapsed_total > 60.0: # 60 second global timeout self.log_activity(f"[SCAN TIMEOUT] Cancelling remaining futures...", "WARN") for f in future_to_pair: f.cancel() break result = future.result() pair = result['pair'] if result['market_data']: current_price = result['market_data']['price'] self.market_data[pair] = {'price': current_price, 'timestamp': time.time()} market_data_all[pair] = result['market_data'] debug_logs.append(f"[DEBUG] {pair}: ${current_price:,.2f}") if result['setup']: all_setups.append(result['setup']) # === SETUP QUEUE: Add to queue immediately === self.setup_queue.add_setup(result['setup'], source="scan_worker") # ============================================ setup_grade = result['setup'].get('grade') if grade_priority.get(setup_grade, 0) > grade_priority.get(best_grade_found, 0): best_grade_found = setup_grade debug_logs.append(result['log']) # PERFORMANCE TRACKING pairs_scanned += len(tier) all_setups.sort(key=lambda x: x.get('score', 0), reverse=True) tier_setup_count = len([s for s in all_setups if s.get('pair') in tier]) self.log_activity(f"[SCAN] {tier_name} complete: {tier_setup_count} setups. Total: {len(all_setups)}. Best: {best_grade_found or 'None'}") # OPTIMIZED: Single pass grade counting with Counter from collections import Counter grade_counts = Counter(s.get('grade') for s in all_setups) s_count = grade_counts.get('S', 0) a_plus_count = grade_counts.get('A+', 0) scan_mode = getattr(self, 'scan_mode', 'fast') # === NEW: SATISFACTION CHECK - Stop when we have enough quality setups === # Count setups by grade b_plus_or_better = sum(1 for s in all_setups if grade_priority.get(s.get('grade'), 0) >= 6) # B+ or better a_or_better = sum(1 for s in all_setups if grade_priority.get(s.get('grade'), 0) >= 8) # A or better # Stop if we have 2+ B+ setups OR 1 A+ setup (for immediate trading) if b_plus_or_better >= 2: self.log_activity(f"[SCAN SATISFIED] ✓ Found {b_plus_or_better} setups (B+ or better). Stopping early!", "SUCCESS") print(f"[SCAN SATISFIED] Early termination: {b_plus_or_better} B+ setups, {a_or_better} A setups") break elif a_or_better >= 1 and tier_num >= 2: self.log_activity(f"[SCAN SATISFIED] ✓ Found {a_or_better} A-grade setups. Stopping early!", "SUCCESS") print(f"[SCAN SATISFIED] Early termination: {a_or_better} A setups found") break # === END SATISFACTION CHECK === if scan_mode == 'fast': # AGGRESSIVE: Only stop early for multiple S-grades in tier 1 if tier_num == 1 and s_count >= 3: self.log_activity(f"[SCAN] Found {s_count} S-grades in tier 1! Stopping early.") break # AGGRESSIVE: Only stop early for tier 2 if we have 5+ A+ setups elif tier_num == 2 and a_plus_count >= 5: self.log_activity(f"[SCAN] Found excellent setups (S={s_count}, A+={a_plus_count}). Stopping early.") break # AGGRESSIVE: Need 15+ setups to stop early elif tier_num <= 2 and len(all_setups) >= 15: self.log_activity(f"[SCAN] Found {len(all_setups)} setups already. Stopping for efficiency.") break else: if tier_num < len(tiers): self.log_activity(f"[SCAN] Thorough mode: Continuing to next tier...") if tier_num < len(tiers): time.sleep(0.2) debug_logs.append(f"[DEBUG] === SCAN SUMMARY: Scanned until tier {tier_num}, {len(all_setups)} setups found ===") debug_logs.append(f"[DEBUG] Best grade: {best_grade_found}, Test mode: {self.test_mode}") # PERFORMANCE TRACKING - Save metrics scan_end_time = time.time() self._perf_scan_time = (scan_end_time - scan_start_time) * 1000 # ms self._perf_pairs_scanned = pairs_scanned if pairs_scanned > 0 else total_pairs self._perf_setups_found = len(all_setups) # Debug log to test tab try: self._update_test_log(f"[color=00FF00]✓ Scan complete: {self._perf_scan_time:.0f}ms, {self._perf_pairs_scanned} pairs, {self._perf_setups_found} setups[/color]") except: pass # FIX: Set _scan_completed immediately in thread context (before Kivy callback) # This prevents race condition where bot checks flag before callback executes self._scan_completed = True self._last_scan_time = time.time() print(f"[SCAN WORKER] Scan completed, _scan_completed = True") from kivy.clock import Clock Clock.schedule_once(lambda dt, r=all_setups, m=market_data_all, ie=immediate_entry, logs=debug_logs: self._scan_complete(r, m, ie, logs), 0) threading.Thread(target=scan_worker, daemon=True).start() def _scan_complete(self, results, market_data=None, immediate_entry=False, debug_logs=None): """Handle scan completion - always mark as complete even on errors.""" try: # Mark scan as completed FIRST - bot can now trade self._scan_completed = True self._last_scan_time = time.time() # DEBUG: Log scan completion with filter info print(f"[SCAN COMPLETE] Results: {len(results) if results else 0} setups") print(f"[SCAN COMPLETE] min_grade_filter={self.min_grade_filter}, test_mode={self.test_mode}, all_limits_off={self.all_limits_off}") if results: grades_found = [s.get('grade') for s in results] print(f"[SCAN COMPLETE] Grades found: {grades_found}") # Track grades for Optimus Brain self._track_setup_grades(results) # FORCE TEST TAB UPDATE print(f"[SCAN COMPLETE] Forcing TEST tab update with perf data: scan_time={getattr(self, '_perf_scan_time', 0)}, pairs={getattr(self, '_perf_pairs_scanned', 0)}, setups={getattr(self, '_perf_setups_found', 0)}") # LOG SCAN BEHAVIOR try: logger = get_behavior_logger(self) logger.log_scan( pairs_count=getattr(self, '_perf_pairs_scanned', 0), setups_found=len(results) if results else 0, filter_applied=self.min_grade_filter if not self.test_mode else "TEST_MODE", duration_ms=getattr(self, '_perf_scan_time', 0) ) except Exception as e: print(f"[BEHAVIOR LOG ERROR] {e}") try: self.update_performance_display() except Exception as e: print(f"[SCAN COMPLETE] Error updating TEST tab: {e}") if results: first = results[0] self.log_activity(f"[b]DEBUG MANUAL SCAN:[/b] {first.get('pair')} entry={first.get('entry',0)}, sl={first.get('stop_loss',0)}, tp={first.get('take_profit1',0)}", "WARN") self.current_setups = results self.analyzing_lbl.text = "" except Exception as e: print(f"[SCAN COMPLETE ERROR] {e}") import traceback traceback.print_exc() # STILL mark as complete even on error self._scan_completed = True self._last_scan_time = time.time() self.current_setups = results if results else [] self.analyzing_lbl.text = "" if debug_logs: for log in debug_logs: self.log_activity(log) self.setups_container.clear_widgets() with self._filter_lock: limits_off = self.all_limits_off if not results: if self.test_mode or limits_off: filter_text = "(test mode - all grades)" else: filter_text = f"(grade filter: A+ to {self.min_grade_filter})" self.setups_status_lbl.text = f"No setups found {filter_text}" self.setups_status_lbl.color = GRAY self.found_setups_lbl.text = "" if hasattr(self, 'best_setup_content'): self.best_setup_content.text = "No setups available.\nTry adjusting scan parameters or check market conditions." self.best_setup_content.color = GRAY self.log_activity(f"Scan complete: No setups found {filter_text}") if market_data and DEBUG_BTC_ONLY: self.log_activity("[DEBUG] Showing market data cards...") for pair, data in market_data.items(): card = self._create_market_data_card(pair, data) self.setups_container.add_widget(card) else: mode_text = "TEST MODE" if self.test_mode else "FILTERED" results.sort(key=lambda x: x.get('score', 0), reverse=True) for setup in results: self.log_setup_found(setup) self.setups_status_lbl.text = f"Scan complete ({mode_text})" self.setups_status_lbl.color = GREEN self.found_setups_lbl.text = f"[b]FOUND {len(results)} SETUP{'S' if len(results) > 1 else ''}[/b]" self.setups_status_lbl.color = GREEN for setup in results: card = SetupCard(setup, on_trade_callback=self.manual_trade) self.setups_container.add_widget(card) # Log trade selection process for multiple setups if len(results) > 1: self.log_trade_selection_process(results, context="SCAN_BEST_SETUP") if results: best = results[0] self.update_best_setup_display(best) if not getattr(self, 'silent_mode', False): self.log_activity(f"═══════════════════════════════════════", "INFO") self.log_activity(f"[b]SCAN COMPLETE:[/b] {len(results)} setup(s) found", "SETUP") a_setups = [s for s in results if s['grade'].startswith('A')] b_setups = [s for s in results if s['grade'].startswith('B')] other_setups = [s for s in results if not s['grade'].startswith(('A', 'B'))] if a_setups: self.log_activity(f" [b]A-Grade ({len(a_setups)}):[/b] " + ", ".join([f"{s['pair']} {s['direction']}" for s in a_setups[:3]]), "SETUP") if any(s['grade'] in ['A+', 'S'] for s in a_setups): self.sound_manager.play('setup_found') if b_setups: self.log_activity(f" [b]B-Grade ({len(b_setups)}):[/b] " + ", ".join([f"{s['pair']} {s['direction']}" for s in b_setups[:3]]), "SETUP") if other_setups: self.log_activity(f" [b]Other ({len(other_setups)}):[/b] " + ", ".join([f"{s['pair']} {s['direction']}" for s in other_setups[:3]]), "SETUP") for i, s in enumerate(results[:3], 1): setup_type = s.get('setup_type', 'STANDARD').replace('_', ' ') # Add confluence info if available confluence_info = "" if s.get('confluence_score', 0) > 0: tf_align = s.get('timeframe_alignment', {}) m15_ok = '✓' if tf_align.get('15m') else '○' h1_ok = '✓' if tf_align.get('1h') else '○' h4_ok = '✓' if tf_align.get('4h') else '○' confluence_info = f" | C:{s['confluence_score']} [{m15_ok}{h1_ok}{h4_ok}]" self.log_activity(f"[b]#{i}[/b] {s['pair']} {s['direction']} | Grade: {s['grade']} | Score: {s['score']}{confluence_info}", "SETUP") self.log_activity(f"═══════════════════════════════════════", "INFO") if immediate_entry and self.bot_engaged and results: # Check if we're still in startup grace period time_since_engaged = time.time() - getattr(self, '_engaged_time', 0) if time_since_engaged < getattr(self, '_startup_grace_period', 0): self.log_activity(f"[b]STARTUP:[/b] Grace period active ({self._startup_grace_period - time_since_engaged:.0f}s remaining) - skipping auto-entry", "INFO") else: self.log_activity("[b]BOT AUTO ENTRY:[/b] Seeking diversified setups...", "TRADE") self._auto_enter_trades(results[:5]) def update_existing_setups(self): if not self.current_setups: self.log_activity("[b]UPDATE:[/b] No setups to update. Run REFRESH first.") return self.setups_status_lbl.text = "UPDATING..." self.setups_status_lbl.color = AMBER self.log_activity(f"[b]UPDATE:[/b] Re-checking {len(self.current_setups)} setup(s)...") pairs_to_check = [s['pair'] for s in self.current_setups] def update_worker(): updated_setups = [] invalidated = [] improved = [] for setup in self.current_setups: pair = setup['pair'] try: klines = self.binance.get_klines(pair, '1h', 50) if not klines: invalidated.append(f"{pair}: No data") continue prices = [float(k[4]) for k in klines] volumes = [float(k[5]) for k in klines] if prices: self.market_data[pair] = {'price': prices[-1], 'timestamp': time.time()} with self._filter_lock: all_limits_val3 = self.all_limits_off new_setup = scan_setups(prices, volumes, pair, no_filter=(self.test_mode or all_limits_val3), min_grade=self.min_grade_filter, scalping_mode=getattr(self, 'scalping_mode_enabled', False), scalp_target_pct=getattr(self, 'scalping_profit_target', 0.004), scalp_stop_pct=getattr(self, 'scalping_stop_loss', 0.003)) if new_setup and new_setup.get('detected'): old_score = setup.get('score', 0) new_score = new_setup.get('score', 0) old_grade = setup.get('grade', '') new_grade = new_setup.get('grade', '') if new_score > old_score + 5: improved.append(f"{pair}: {old_grade}→{new_grade} ({old_score}→{new_score})") updated_setups.append(new_setup) else: invalidated.append(f"{pair}: Conditions changed") except Exception as e: invalidated.append(f"{pair}: Error - {e}") from kivy.clock import Clock Clock.schedule_once(lambda dt, u=updated_setups, i=invalidated, imp=improved: self._update_complete(u, i, imp), 0) threading.Thread(target=update_worker, daemon=True).start() def _update_complete(self, updated_setups, invalidated, improved): self.current_setups = updated_setups self.setups_container.clear_widgets() if invalidated: for msg in invalidated[:3]: self.log_activity(f"[b]INVALIDATED:[/b] {msg}") if improved: for msg in improved[:3]: self.log_activity(f"[b]IMPROVED:[/b] {msg}") if not updated_setups: self.setups_status_lbl.text = "All setups invalidated" self.setups_status_lbl.color = RED if hasattr(self, 'best_setup_content'): self.best_setup_content.text = "All setups invalidated.\nRun REFRESH to find new setups." self.best_setup_content.color = GRAY self.log_activity("[b]UPDATE:[/b] All setups invalidated") else: self.setups_status_lbl.text = f"[b]{len(updated_setups)} SETUP{'S' if len(updated_setups) > 1 else ''} VALID[/b]" self.setups_status_lbl.color = GREEN for setup in updated_setups: card = SetupCard(setup, on_trade_callback=self.manual_trade) self.setups_container.add_widget(card) if updated_setups: best = updated_setups[0] self.update_best_setup_display(best) self.log_activity(f"[b]UPDATE:[/b] {len(updated_setups)} setup(s) still valid") def validate_setup_before_trade(self, setup): """ Comprehensive pre-trade validation. Returns validated setup or None if rejected. """ pair = setup.get('pair') if not pair: return None # Skip validation if disabled (but keep for auto-trades now) with self._filter_lock: if not self.pre_trade_validation: return setup # === GRADE CHECK (if Market Brain strict mode) === grade = setup.get('grade', 'C') if getattr(self, 'market_brain_strict', False): # Strict mode: Only accept B or higher allowed_grades = ['A+', 'A', 'A-', 'B+', 'B', 'B-'] if grade not in allowed_grades: self.log_activity(f"[VALIDATION] {pair}: Grade {grade} rejected (strict mode requires B or higher)", "WARN") return None else: # Normal mode: Accept ALL grades (F to A+) allowed_grades = ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F'] if grade not in allowed_grades: self.log_activity(f"[VALIDATION] {pair}: Grade {grade} rejected (invalid grade)", "WARN") return None # === R/R RATIO CHECK === entry_price = setup.get('entry', 0) stop_loss = setup.get('stop_loss', 0) take_profit = setup.get('take_profit1', 0) if entry_price > 0 and stop_loss > 0 and take_profit > 0: risk = abs(entry_price - stop_loss) reward = abs(take_profit - entry_price) if risk > 0: rr_ratio = reward / risk min_rr = getattr(self, 'min_rr_ratio', 1.5) if rr_ratio < min_rr: self.log_activity(f"[VALIDATION] {pair}: R/R 1:{rr_ratio:.1f} below minimum 1:{min_rr}", "WARN") return None else: self.log_activity(f"[VALIDATION] {pair}: Invalid risk calculation (SL too close to entry)", "WARN") return None else: self.log_activity(f"[VALIDATION] {pair}: Missing price levels for R/R calc", "WARN") return None try: # === 1. PRICE CHECK === klines = self.binance.get_klines(pair, '1h', 10) if not klines or len(klines) < 5: self.log_activity(f"[VALIDATION] {pair}: Insufficient price data", "WARN") return None current_price = float(klines[-1][4]) entry_price = setup.get('entry', 0) direction = setup.get('direction', 'LONG') if entry_price <= 0: self.log_activity(f"[VALIDATION] {pair}: Invalid entry price", "ERROR") return None # Check if price moved too far from entry if direction == 'LONG': if current_price > entry_price * 1.03: self.log_activity(f"[VALIDATION] {pair}: Price +{(current_price/entry_price-1)*100:.1f}% above entry - MISSED", "WARN") return None else: # SHORT if current_price < entry_price * 0.97: self.log_activity(f"[VALIDATION] {pair}: Price -{(1-current_price/entry_price)*100:.1f}% below entry - MISSED", "WARN") return None # === 2. VOLUME CHECK (Minimum $1M 24h volume) === volume_24h = self.market_data.get(pair, {}).get('volume_24h', 0) if volume_24h < 750_000: self.log_activity(f"[VALIDATION] {pair}: Volume ${volume_24h:,.0f} below $750K minimum", "WARN") return None # === 3. SPREAD CHECK (Max 0.5%) === orderbook = self._get_cached_orderbook(pair, limit=10) if orderbook and orderbook.get('bids') and orderbook.get('asks'): best_bid = float(orderbook['bids'][0][0]) best_ask = float(orderbook['asks'][0][0]) spread_pct = (best_ask - best_bid) / current_price * 100 if spread_pct > 0.5: # More than 0.5% spread self.log_activity(f"[VALIDATION] {pair}: Spread {spread_pct:.2f}% too wide (>0.5%)", "WARN") return None # === 4. MULTI-TIMEFRAME ALIGNMENT CHECK === # Require at least 2 of 3 timeframes to align with trade direction aligned_count = 0 tf_checks = [] # Check 15m trend klines_15m = self._get_cached_klines(pair, '15m', 20) if klines_15m and len(klines_15m) >= 10: prices_15m = [float(k[4]) for k in klines_15m] ema_fast_15m = sum(prices_15m[-5:]) / 5 ema_slow_15m = sum(prices_15m[-10:]) / 10 if direction == 'LONG' and ema_fast_15m > ema_slow_15m: aligned_count += 1 tf_checks.append("15m UP") elif direction == 'SHORT' and ema_fast_15m < ema_slow_15m: aligned_count += 1 tf_checks.append("15m DOWN") else: tf_checks.append("15m WRONG") # Check 1h trend prices_1h = [float(k[4]) for k in klines] if len(prices_1h) >= 10: ema_fast_1h = sum(prices_1h[-5:]) / 5 ema_slow_1h = sum(prices_1h[-10:]) / 10 if direction == 'LONG' and ema_fast_1h > ema_slow_1h: aligned_count += 1 tf_checks.append("1h UP") elif direction == 'SHORT' and ema_fast_1h < ema_slow_1h: aligned_count += 1 tf_checks.append("1h DOWN") else: tf_checks.append("1h WRONG") # Check 4h trend (from setup data if available) setup_trend = setup.get('trend', setup.get('timeframe_alignment', {})) if isinstance(setup_trend, dict): h4_align = setup_trend.get('4h', setup_trend.get('h4', False)) if h4_align: aligned_count += 1 tf_checks.append("4h ALIGN") else: tf_checks.append("4h N/A") # Require at least 2 timeframes aligned if aligned_count < 2: self.log_activity(f"[VALIDATION] {pair}: Only {aligned_count}/3 timeframes align ({', '.join(tf_checks)})", "WARN") return None # === 5. RSI ENTRY TIMING CHECK (CRITICAL FIX) === # Calculate RSI and reject if overbought/oversold against direction try: klines_15m = self._get_cached_klines(pair, '15m', 30) if klines_15m and len(klines_15m) >= 20: prices_15m = [float(k[4]) for k in klines_15m] # Calculate RSI using standalone function rsi = calculate_rsi(prices_15m, 14) # STRICT RSI CHECKS if direction == 'LONG': if rsi > 70: self.log_activity(f"[VALIDATION] {pair}: RSI {rsi:.1f} > 70 (overbought) - REJECTING LONG", "ERROR") return None elif rsi > 65: self.log_activity(f"[VALIDATION] {pair}: RSI {rsi:.1f} elevated (>65) - MARGINAL", "WARN") else: # SHORT if rsi < 30: self.log_activity(f"[VALIDATION] {pair}: RSI {rsi:.1f} < 30 (oversold) - REJECTING SHORT", "ERROR") return None elif rsi < 35: self.log_activity(f"[VALIDATION] {pair}: RSI {rsi:.1f} low (<35) - MARGINAL", "WARN") # Log favorable RSI if (direction == 'LONG' and rsi < 50) or (direction == 'SHORT' and rsi > 50): self.log_activity(f"[VALIDATION] {pair}: RSI {rsi:.1f} favorable for {direction}", "SUCCESS") except Exception as e: print(f"[RSI CHECK ERROR] {pair}: {e}") # Continue on RSI error - don't block trade # === 6. PRECISION INDICATOR CHECKS (NEW) === # Only apply if indicators are initialized if hasattr(self, 'divergence_scanner') and hasattr(self, 'adx_filter') and hasattr(self, 'liquidity_sweep_detector'): try: # Get klines for indicator calculations klines_15m = self._get_cached_klines(pair, '15m', 30) if klines_15m and len(klines_15m) >= 20: prices = [float(k[4]) for k in klines_15m] highs = [float(k[2]) for k in klines_15m] lows = [float(k[3]) for k in klines_15m] closes = [float(k[4]) for k in klines_15m] # 5a. ADX Trend Filter adx_result = self.adx_filter.should_trade(highs, lows, closes, direction) if not adx_result['trade_allowed']: self.log_activity(f"[VALIDATION] {pair}: {adx_result['reason']}", "WARN") return None # 5b. RSI Divergence Check divergence = self.divergence_scanner.detect_divergence(prices, pair) if divergence: div_type = divergence['type'] div_strength = divergence['strength'] # Reject if divergence is against our direction if direction == 'LONG' and div_type == 'bearish' and div_strength in ['medium', 'strong']: self.log_activity(f"[VALIDATION] {pair}: Bearish divergence detected ({div_strength}) - avoiding LONG", "WARN") return None elif direction == 'SHORT' and div_type == 'bullish' and div_strength in ['medium', 'strong']: self.log_activity(f"[VALIDATION] {pair}: Bullish divergence detected ({div_strength}) - avoiding SHORT", "WARN") return None # Log confirming divergence elif direction == 'LONG' and div_type == 'bullish': self.log_activity(f"[VALIDATION] {pair}: Bullish divergence confirmed ({div_strength})", "SUCCESS") elif direction == 'SHORT' and div_type == 'bearish': self.log_activity(f"[VALIDATION] {pair}: Bearish divergence confirmed ({div_strength})", "SUCCESS") # 5c. Liquidity Sweep Check sweep = self.liquidity_sweep_detector.detect_sweep(pair, highs, lows, closes) if sweep: sweep_type = sweep['type'] sweep_strength = sweep['strength'] # Confirm if sweep aligns with direction if direction == 'LONG' and sweep_type == 'bullish_sweep': self.log_activity(f"[VALIDATION] {pair}: Bullish sweep confirmed ({sweep_strength}) - potential reversal", "SUCCESS") elif direction == 'SHORT' and sweep_type == 'bearish_sweep': self.log_activity(f"[VALIDATION] {pair}: Bearish sweep confirmed ({sweep_strength}) - potential reversal", "SUCCESS") # === 6. NEW TODO LIST FEATURES CHECKS === # 6a. VWAP Bands - Check mean reversion opportunity if hasattr(self, 'vwap_bands'): volumes = [float(k[5]) for k in klines_15m] vwap_data = self.vwap_bands.calculate(prices, volumes) if vwap_data: mr_opportunity = self.vwap_bands.check_mean_reversion_opportunity(vwap_data, direction) if mr_opportunity.get('is_opportunity'): self.log_activity(f"[VALIDATION] {pair}: VWAP {mr_opportunity['recommendation']} (confidence: {mr_opportunity['confidence']:.0%})", "SUCCESS") elif vwap_data['band_level'] >= 2: # Price at extreme but not favorable direction self.log_activity(f"[VALIDATION] {pair}: VWAP warning - price at {vwap_data['band_level']}σ extreme", "WARN") # 6b. Volume Profile - Check price position relative to POC if hasattr(self, 'volume_profile'): volumes = [float(k[5]) for k in klines_15m] vp_data = self.volume_profile.calculate(prices, volumes) if vp_data: position_data = self.volume_profile.get_position(current_price, vp_data) self.log_activity(f"[VALIDATION] {pair}: Volume Profile - {position_data['position']} (POC: ${vp_data['poc']:.2f}, VAH: ${vp_data['vah']:.2f}, VAL: ${vp_data['val']:.2f})", "SUCCESS") # 6c. CVD Monitor - Check order flow if hasattr(self, 'cvd_monitor'): cvd_data = self.cvd_monitor.calculate(klines_15m) if cvd_data: # Reject if order flow against direction if direction == 'LONG' and cvd_data['dominance'] == 'sellers': self.log_activity(f"[VALIDATION] {pair}: CVD shows selling pressure ({cvd_data['buy_ratio']:.0%} buy) - rejecting LONG", "WARN") return None elif direction == 'SHORT' and cvd_data['dominance'] == 'buyers': self.log_activity(f"[VALIDATION] {pair}: CVD shows buying pressure ({cvd_data['buy_ratio']:.0%} buy) - rejecting SHORT", "WARN") return None else: self.log_activity(f"[VALIDATION] {pair}: CVD confirms direction ({cvd_data['dominance']}, {cvd_data['buy_ratio']:.0%} buy)", "SUCCESS") # 6d. Pattern Recognition - Check for patterns if hasattr(self, 'pattern_recognizer'): klines_1h = self._get_cached_klines(pair, '1h', 30) if klines_1h and len(klines_1h) >= 20: h_prices = [float(k[4]) for k in klines_1h] h_highs = [float(k[2]) for k in klines_1h] h_lows = [float(k[3]) for k in klines_1h] flag = self.pattern_recognizer.detect_flag(h_highs, h_lows, h_prices) if flag: self.log_activity(f"[VALIDATION] {pair}: {flag['direction'].upper()} {flag['type']} detected ({flag['completion']:.0%} complete)", "SUCCESS") # 6e. Market Condition Scorer - Check overall conditions if hasattr(self, 'market_scorer'): indicators_data = { 'adx': adx_result['adx_data']['adx'] if 'adx_result' in dir() else 25, 'trend': 'bullish' if direction == 'LONG' else 'bearish' if direction == 'SHORT' else 'flat', 'volume_ratio': volume_24h / 1_000_000 if volume_24h > 0 else 1.0, 'atr_pct': 2.0, # Default assumption 'correlation': 0.8, # Default assumption 'divergence': divergence is not None if 'divergence' in dir() else False, 'cvd_trend': cvd_data.get('dominance', 'neutral') if 'cvd_data' in dir() else 'neutral' } score_result = self.market_scorer.calculate(indicators_data) if score_result['total_score'] < 50: self.log_activity(f"[VALIDATION] {pair}: Market score {score_result['total_score']}/100 too low ({score_result['recommendation']})", "WARN") return None else: self.log_activity(f"[VALIDATION] {pair}: Market score {score_result['total_score']}/100 ({score_result['recommendation']})", "SUCCESS") self.log_activity(f"[VALIDATION] {pair}: Indicators PASSED | ADX: {adx_result['adx_data']['adx']:.1f} ({adx_result['adx_data']['strength']})", "SUCCESS") except Exception as e: print(f"[INDICATOR ERROR] {pair}: {e}") # Continue on error - don't block trade due to indicator issues # === 7. ENTRY TIMING VALIDATION (RSI + StochRSI + Orderbook) === if hasattr(self, 'entry_timing'): try: # Get fresh klines for RSI/StochRSI klines_15m = self._get_cached_klines(pair, '15m', 30) # Get fresh orderbook orderbook = self._get_cached_orderbook(pair, limit=20) if klines_15m and orderbook: timing_result = self.entry_timing.validate_entry_timing( pair, direction, current_price, klines_15m, orderbook ) rsi_val = timing_result.get('rsi', 0) stoch_k = timing_result.get('stochrsi', {}).get('k', 0) if timing_result.get('stochrsi') else 0 score = timing_result.get('score', 0) if not timing_result['valid']: self.log_activity(f"[VALIDATION] {pair}: Entry timing REJECTED (score: {score}) - {timing_result['recommendation']}", "ERROR") return None elif score < 70: self.log_activity(f"[VALIDATION] {pair}: Entry timing WARNING (score: {score}) - {timing_result['recommendation']}", "WARN") # Continue but log warning else: self.log_activity(f"[VALIDATION] {pair}: Entry timing EXCELLENT (score: {score}) RSI:{rsi_val:.1f} Stoch:{stoch_k:.1f}", "SUCCESS") # Use adjusted entry if significantly better adjusted_entry = timing_result.get('adjusted_entry', current_price) if abs(adjusted_entry - current_price) / current_price > 0.001: # > 0.1% difference self.log_activity(f"[VALIDATION] {pair}: Suggested entry adjusted from ${current_price:.4f} to ${adjusted_entry:.4f}", "INFO") except Exception as e: print(f"[ENTRY TIMING ERROR] {pair}: {e}") # Continue on error # === 8. ADAPTIVE ENTRY VALIDATION (BTC/DXY + Momentum Regime) === if hasattr(self, 'adaptive_entry'): try: # Get fresh klines klines_15m = self._get_cached_klines(pair, '15m', 30) orderbook = self._get_cached_orderbook(pair, limit=20) if klines_15m and orderbook: adaptive_result = self.adaptive_entry.analyze_adaptive_entry( pair, direction, current_price, klines_15m, orderbook ) mode = adaptive_result.get('mode', 'mean_reversion') score = adaptive_result.get('score', 0) rsi_val = adaptive_result.get('rsi', 0) regime = adaptive_result.get('regime', {}) btc_data = regime.get('btc_data', {}) # Log the regime self.log_activity(f"[ADAPTIVE] {pair}: Regime={regime.get('regime', 'unknown')} | " f"BTC={btc_data.get('trend', 'unknown')}/{btc_data.get('momentum', 'weak')} | " f"Rec={regime.get('recommendation', 'avoid')}", "INFO") if not adaptive_result['valid']: self.log_activity(f"[ADAPTIVE] {pair}: REJECTED (score: {score}) - {adaptive_result['recommendation']}", "ERROR") return None # Log the mode and score if mode == 'scalp': self.log_activity(f"[ADAPTIVE] {pair}: SCALP MODE ACTIVATED (score: {score}) RSI:{rsi_val:.1f} | " f"SL: ${adaptive_result['stop_loss']:.4f} TP: ${adaptive_result['take_profit']:.4f}", "SUCCESS") # Override setup with scalp parameters setup['mode'] = 'scalp' setup['adaptive_stop_loss'] = adaptive_result['stop_loss'] setup['adaptive_take_profit'] = adaptive_result['take_profit'] elif mode == 'momentum': self.log_activity(f"[ADAPTIVE] {pair}: MOMENTUM MODE (score: {score}) RSI:{rsi_val:.1f} | " f"BTC aligned: {adaptive_result.get('btc_aligned', False)}", "SUCCESS") setup['mode'] = 'momentum' else: self.log_activity(f"[ADAPTIVE] {pair}: MEAN REVERSION (score: {score}) RSI:{rsi_val:.1f}", "SUCCESS") setup['mode'] = 'mean_reversion' # Store adaptive data in setup for later use setup['adaptive_score'] = score setup['adaptive_regime'] = regime.get('regime') setup['btc_aligned'] = adaptive_result.get('btc_aligned', False) except Exception as e: print(f"[ADAPTIVE ENTRY ERROR] {pair}: {e}") # Continue on error self.log_activity(f"[VALIDATION] {pair}: PASSED | {aligned_count}/3 TF align | Vol: ${volume_24h/1e6:.2f}M" if volume_24h >= 1_000_000 else f"[VALIDATION] {pair}: PASSED | {aligned_count}/3 TF align | Vol: ${volume_24h/1e3:.0f}K", "SUCCESS") return setup except Exception as e: print(f"[VALIDATION ERROR] {pair}: {e}") # On error, allow trade (fail open for safety in case of temporary issues) return setup def _create_market_data_card(self, pair, data): card = BorderedCard(size_hint_y=None, height=dp(120)) card.orientation = 'vertical' card.padding = dp(10) card.spacing = dp(4) header = BoxLayout(size_hint_y=0.4) header.add_widget(Label( text=f"[b]{pair}[/b]", markup=True, color=GOLD, font_size=sp(14), size_hint_x=0.6 )) change = data.get('change_24h', 0) change_color = GREEN if change >= 0 else RED header.add_widget(Label( text=f"{change:+.2f}%", markup=True, color=change_color, font_size=sp(12), size_hint_x=0.4 )) card.add_widget(header) price = data.get('price', 0) card.add_widget(Label( text=f"Price: ${price:,.2f}", color=WHITE, font_size=sp(11), size_hint_y=0.3 )) vol = data.get('volume_24h', 0) card.add_widget(Label( text=f"24h Vol: {vol:,.0f}", color=GRAY, font_size=sp(10), size_hint_y=0.3 )) return card @mainthread def update_best_setup_display(self, setup): # Store current setup for R/R adjustment self._current_top_setup = setup # Skip UI update if best_setup_content doesn't exist (removed from SETUPS tab) if not hasattr(self, 'best_setup_content'): return direction = setup.get('direction', 'LONG') dir_color = GREEN if direction == 'LONG' else RED signal_type = setup.get('signal_type', 'THOR') grade = setup.get('grade', 'B-') factors = setup.get('factors', []) pair = setup.get('pair', 'BTCUSDC') entry = setup.get('entry', 0) tp = setup.get('take_profit1', 0) sl = setup.get('stop_loss', 0) rr = setup.get('rr_ratio', 0) factors_text = " | ".join(factors[:2]) if factors else "" # Convert color tuple to hex for markup def rgb_to_hex(rgb): return f"{int(rgb[0]*255):02x}{int(rgb[1]*255):02x}{int(rgb[2]*255):02x}" dir_color_hex = rgb_to_hex(dir_color) self.best_setup_content.text = ( f"[color={dir_color_hex}]{setup['pair']} {direction} ({signal_type})[/color]\n" f"Grade: [b]{grade}[/b] | Score: [b]{setup['score']}[/b] | R/R: [b]1:{rr}[/b]\n" f"Entry: [b]{format_price(entry, pair)}[/b] | TP: [b]{format_price(tp, pair)}[/b] | SL: [b]{format_price(sl, pair)}[/b]\n" f"[color=aaaaaa]{factors_text}[/color]" ) self.best_setup_content.color = WHITE # Update R/R display label if hasattr(self, 'rr_display_label'): self.rr_display_label.text = f"[b]1:{rr:.1f}[/b]" def execute_trade(self, setup, skip_validation=False, is_bot_auto=False): pair_dbg = setup.get('pair', 'Unknown') entry_dbg = setup.get('entry', 0) sl_dbg = setup.get('stop_loss', 0) tp_dbg = setup.get('take_profit1', 0) if entry_dbg == 0 or sl_dbg == 0 or tp_dbg == 0: self.log_activity(f"[b]DEBUG execute_trade:[/b] {pair_dbg} entry={entry_dbg}, sl={sl_dbg}, tp={tp_dbg}", "WARN") if not skip_validation: validated_setup = self.validate_setup_before_trade(setup) if not validated_setup: self.log_activity(f"[b]ABORT:[/b] Setup validation failed for {setup.get('pair', 'Unknown')}") return None setup = validated_setup else: trade_type = "BOT AUTO" if is_bot_auto else "EXECUTE" self.log_activity(f"[b]{trade_type}:[/b] Using pre-validated setup for {setup.get('pair', 'Unknown')}") pair = setup.get('pair', 'Unknown') direction = setup.get('direction', 'LONG') available = self.position_manager.get_available_balance(self.total_asset) if available < self.total_asset * 0.10: self.log_activity(f"[b]ERROR:[/b] Insufficient balance (${available:.2f})") return None active_pairs = [p['pair'] for p in self.position_manager.positions if p['status'] == 'OPEN'] if setup['pair'] in active_pairs: self.log_activity(f"[b]SKIP:[/b] Already have position in {setup['pair']}") return None # === MAX SIMULTANEOUS CHECK === open_count = len(active_pairs) if open_count >= self.max_simultaneous: self.log_activity(f"[b]LIMIT:[/b] Max positions reached ({open_count}/{self.max_simultaneous})", "WARN") return None # === END MAX SIMULTANEOUS CHECK === # Get setup-specific market parameters from Market Brain market_params = None if hasattr(self, 'market_brain') and self.market_brain: market_params = self.market_brain.get_position_params(base_size=1.0, setup=setup) # OPTIMUS OVERRIDE: Use Optimus config if active and setup qualifies if getattr(self, 'optimus_override', False) and self._optimus_config: optimus_params = self._get_optimus_trade_params(setup) if optimus_params: market_params.update(optimus_params) self.log_activity( f"[OPTIMUS: OVERRIDE] {pair} | Pattern: {optimus_params['pattern']} | " f"Size: {optimus_params['size_multiplier']:.2f}x | Lev: {optimus_params['leverage']}x", "INFO" ) # Log the trade pattern pattern = market_params.get('trade_pattern', 'standard') size_mult = market_params.get('size_multiplier', 1.0) lev = market_params.get('leverage', 3) self.log_activity( f"[BRAIN: TRADE] {pair} | Pattern: {pattern} | Size: {size_mult:.2f}x | Lev: {lev}x", "INFO" ) # LOG TRADE DECISION try: logger = get_behavior_logger(self) reasons = [ f"grade:{setup.get('grade','?')}", f"pattern:{pattern}", f"rr:{setup.get('rr_ratio',0):.1f}" ] logger.log_trade_decision(pair, "EXECUTE", reasons) except Exception as e: print(f"[BEHAVIOR LOG ERROR] {e}") # Apply Market Brain stop strategy adjustments stop_strategy = market_params.get('stop_strategy', 'normal') entry = setup.get('entry', 0) current_sl = setup.get('stop_loss', 0) if entry > 0 and current_sl > 0: current_sl_pct = abs(entry - current_sl) / entry new_sl = current_sl if stop_strategy == 'tight': # Tight stop: 20% tighter than default new_sl_pct = current_sl_pct * 0.8 if direction == 'LONG': new_sl = entry * (1 - new_sl_pct) else: new_sl = entry * (1 + new_sl_pct) self.log_activity(f"[BRAIN: STOP] Tight stop: {new_sl_pct*100:.2f}% (was {current_sl_pct*100:.2f}%)", "INFO") elif stop_strategy == 'wide': # Wide stop: 30% wider for more breathing room new_sl_pct = current_sl_pct * 1.3 if direction == 'LONG': new_sl = entry * (1 - new_sl_pct) else: new_sl = entry * (1 + new_sl_pct) self.log_activity(f"[BRAIN: STOP] Wide stop: {new_sl_pct*100:.2f}% (was {current_sl_pct*100:.2f}%)", "INFO") elif stop_strategy == 'breakeven_focus': # Move SL closer to entry for quick breakeven protection new_sl_pct = current_sl_pct * 0.6 if direction == 'LONG': new_sl = entry * (1 - new_sl_pct) else: new_sl = entry * (1 + new_sl_pct) self.log_activity(f"[BRAIN: STOP] Breakeven focus: {new_sl_pct*100:.2f}% (was {current_sl_pct*100:.2f}%)", "INFO") # Only update if new SL is valid if new_sl > 0 and ((direction == 'LONG' and new_sl < entry) or (direction == 'SHORT' and new_sl > entry)): setup['stop_loss'] = round(new_sl, 2) self.log_activity(f"[BRAIN: STOP] Adjusted SL: ${current_sl:.2f} -> ${new_sl:.2f}", "INFO") # MAGIC ENTRY: Dynamic sizing if hasattr(self, 'magic_entry') and self.magic_entry: base_size = available * 0.5 signals = { 'momentum_aligned': setup.get('momentum_aligned', True), 'volume_confirmed': setup.get('volume_above_avg', False), 'structure_intact': not setup.get('structure_broken', False), 'rsi_favorable': True } magic_size = self.magic_entry.calculate_dynamic_size(base_size, setup, signals) confidence = setup.get('confidence', 70) grade = setup.get('grade', 'B') splits = self.magic_entry.calculate_split_sizes(magic_size, confidence, grade) self.log_activity( f"[MAGIC ENTRY] {pair} Grade {grade} | Size ${magic_size:.2f} | " f"Split {splits[0][0]*100:.0f}/{splits[1][0]*100:.0f}/{splits[2][0]*100:.0f}", "INFO" ) setup['_magic_entry'] = {'size': magic_size, 'splits': splits} # Store in setup for use during position opening setup['_market_params'] = market_params position = self.position_manager.open_position(setup, available, self.paper_mode, self.binance, market_params=market_params) if position: mode_text = "PAPER" if self.paper_mode else "LIVE" source = "BOT" if is_bot_auto else "MANUAL" # Get actual leverage from market_params if available, else default actual_lev = market_params.get('leverage', self.position_manager.leverage) if market_params else self.position_manager.leverage size_mult = market_params.get('size_multiplier', 1.0) if market_params else 1.0 pattern = market_params.get('trade_pattern', 'standard') if market_params else 'standard' target_rr = position.get('target_rr', 1.5) is_ph = position.get('is_power_hour', False) ph_tag = " [POWER HOUR]" if is_ph else "" # Calculate percentages safely entry_price = position.get('entry_price', 0) if entry_price > 0: sl_pct = abs(entry_price - position['stop_loss']) / entry_price * 100 tp1_pct = abs(position['take_profit1'] - entry_price) / entry_price * 100 else: sl_pct = 0 tp1_pct = 0 entry_msg = ( f"[b]{source} {mode_text} ENTRY:[/b] {position['pair']} {position['direction']} {actual_lev}x (Brain: {pattern}){ph_tag}\n" f" Qty: {position['quantity']:.4f} | Value: ${position['position_value']:.2f} | SizeMult: {size_mult:.2f}x\n" f" Entry: ${position['entry_price']:.4f} | R/R: 1:{target_rr:.1f}\n" f" SL: ${position['stop_loss']:.2f} ({sl_pct:.2f}%)\n" f" TP1: ${position['take_profit1']:.2f} ({tp1_pct:.2f}%)\n" f" TP2: Trail from ${position['take_profit1']:.2f} with 0.3%" ) self.log_activity(entry_msg) # Log trade entry with TradeLogger confluence_data = setup.get('confluence_data', { 'confluence_score': 0, 'timeframe_alignment': {'15m': False, '1h': False, '4h': False}, 'm15_direction': '', 'h1_direction': '', 'h4_direction': '' }) self.trade_logger.log_trade_entry( trade_id=position['id'], setup=setup, confluence_data=confluence_data, entry_price=position['entry_price'], signal_price=setup.get('entry', position['entry_price']), paper_mode=self.paper_mode ) self.backtest_logger.log_trade_execution( pair=pair, direction=direction, setup=setup, is_bot_auto=is_bot_auto, validation_used=not skip_validation ) self.update_positions_display() self.update_assets_display() return position else: self.log_activity("[b]ERROR:[/b] Failed to open position") return None def attempt_trade(self): self.check_and_enter_trade() def manual_trade(self, setup): self.log_activity(f"[b]MANUAL TRADE:[/b] {setup['pair']} {setup['direction']}") return self.execute_trade(setup) def close_position_manual(self, position_id): """Close a single position with confirmation dialog.""" for pos in self.position_manager.positions: if pos['id'] == position_id and pos['status'] == 'OPEN': # Show confirmation dialog content = BoxLayout(orientation='vertical', spacing=dp(10), padding=dp(20)) content.add_widget(Label( text=f"[b]Close position?[/b]\n\n{pos['pair']} {pos['direction']}\nEntry: ${pos['entry_price']:.2f}\nCurrent PnL: ${pos.get('unrealized_pnl', 0):+.2f}", markup=True, color=WHITE, font_size=sp(12) )) btn_box = BoxLayout(spacing=dp(10), size_hint_y=0.4) yes_btn = StyledButton(text="[b]CLOSE[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(11), radius=10) no_btn = StyledButton(text="[b]CANCEL[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(11), radius=10) popup = Popup(title="Confirm Close", content=content, size_hint=(0.8, 0.45), background_color=CARD_BG) def do_close(): popup.dismiss() self._execute_close_position(position_id) def cancel(): popup.dismiss() yes_btn.bind(on_press=lambda x: do_close()) no_btn.bind(on_press=lambda x: cancel()) btn_box.add_widget(yes_btn) btn_box.add_widget(no_btn) content.add_widget(btn_box) popup.open() return True return False def _execute_close_position(self, position_id): """Execute the actual position close after confirmation.""" for pos in self.position_manager.positions: if pos['id'] == position_id and pos['status'] == 'OPEN': current = self.market_data.get(pos['pair'], {}).get('price', pos['entry_price']) remaining_qty = pos['quantity'] - pos.get('closed_quantity', 0) pnl = self.position_manager.close_position(position_id, current, "MANUAL_CLOSE", remaining_qty) if not pos.get('paper', True) and self.api_key and self.api_secret: try: side = "SELL" if pos['direction'] == "LONG" else "BUY" self.binance.close_position(pos['pair'], side, round(remaining_qty, 6)) for order_type, order_id in pos.get('orders', {}).items(): if order_id: self.binance.cancel_order(pos['pair'], order_id) except Exception as e: print(f"Error closing position: {e}") self.log_activity(f"[b]CLOSED:[/b] {pos['pair']} | PnL: ${pnl:+.2f}") self.update_positions_display() self.update_assets_display() return True return False def reset_all_positions(self, instance=None): """Reset all positions with confirmation dialog.""" open_count = len([p for p in self.position_manager.positions if p['status'] == 'OPEN']) total_count = len(self.position_manager.positions) if open_count == 0 and total_count == 0: self.log_activity("[b]RESET:[/b] No positions to clear", "INFO") return # Show confirmation dialog content = BoxLayout(orientation='vertical', spacing=dp(10), padding=dp(20)) content.add_widget(Label( text=f"[b]Clear ALL positions?[/b]\n\nThis will delete {open_count} open and {total_count} total positions without closing them!\n\nThis action cannot be undone!", markup=True, color=WHITE, font_size=sp(11) )) btn_box = BoxLayout(spacing=dp(10), size_hint_y=0.4) yes_btn = StyledButton(text="[b]CLEAR ALL[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(11), radius=10) no_btn = StyledButton(text="[b]CANCEL[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(11), radius=10) popup = Popup(title="Confirm Reset", content=content, size_hint=(0.85, 0.5), background_color=CARD_BG) def do_reset(): popup.dismiss() self.position_manager.positions = [] self.log_activity(f"[b]RESET:[/b] Cleared {open_count} open, {total_count} total positions", "TRADE") self.update_positions_display() self.update_assets_display() def cancel(): popup.dismiss() yes_btn.bind(on_press=lambda x: do_reset()) no_btn.bind(on_press=lambda x: cancel()) btn_box.add_widget(yes_btn) btn_box.add_widget(no_btn) content.add_widget(btn_box) popup.open() def close_all_positions(self, instance): content = BoxLayout(orientation='vertical', spacing=dp(10), padding=dp(20)) content.add_widget(Label(text="[b]Close ALL positions?[/b]\n\nThis cannot be undone!", markup=True, color=WHITE, font_size=sp(12))) btn_box = BoxLayout(spacing=dp(10), size_hint_y=0.4) yes_btn = StyledButton(text="[b]YES, CLOSE ALL[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(11), radius=10) no_btn = StyledButton(text="[b]CANCEL[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(11), radius=10) popup = Popup(title="Confirm", content=content, size_hint=(0.8, 0.4), background_color=CARD_BG) def do_close(): count, total_pnl = self.position_manager.close_all_positions("MANUAL_CLOSE_ALL") for pos in self.position_manager.positions: if pos.get('paper', True): continue if self.api_key and self.api_secret: try: side = "SELL" if pos['direction'] == "LONG" else "BUY" remaining_qty = pos['quantity'] - pos.get('closed_quantity', 0) self.binance.close_position(pos['pair'], side, round(remaining_qty, 6)) for order_type, order_id in pos.get('orders', {}).items(): if order_id: self.binance.cancel_order(pos['pair'], order_id) except Exception as e: print(f"Error closing position: {e}") self.log_activity(f"[b]CLOSED ALL:[/b] {count} positions | Total PnL: ${total_pnl:+.2f}") self.update_positions_display() self.update_assets_display() popup.dismiss() yes_btn.bind(on_press=lambda x: do_close()) no_btn.bind(on_press=popup.dismiss) btn_box.add_widget(yes_btn) btn_box.add_widget(no_btn) content.add_widget(btn_box) popup.open() @mainthread def update_positions_display(self, dt=None): print(f"[DEBUG] update_positions_display called, has cloud: {hasattr(self, 'cloud')}") # Throttle: skip if ran less than 5 seconds ago now = time.time() last_update = getattr(self, '_last_positions_display_update', 0) # ALWAYS push to cloud, even if throttled if hasattr(self, 'cloud'): print("[DEBUG] Calling cloud.push_state...") self.cloud.push_state(self) else: print("[DEBUG] NO self.cloud attribute!") if now - last_update < 5: return self._last_positions_display_update = now self.positions_container.clear_widgets() open_positions = [p for p in self.position_manager.positions if p['status'] == 'OPEN'] # === REPAIR EXISTING POSITIONS WITH BAD DATA (throttled: once per position) === now = time.time() for pos in open_positions: pair = pos.get('pair', '') # Only repair if not repaired in last 60 seconds last_repair = pos.get('_last_repair_check', 0) if now - last_repair < 60: continue pos['_last_repair_check'] = now entry = pos.get('entry_price', 0) direction = pos.get('direction', 'LONG') sl = pos.get('stop_loss', 0) tp1 = pos.get('take_profit1', 0) if entry == 0 or not pair: continue repairs = [] # Fix SL above entry for LONG if direction == 'LONG' and sl >= entry: pos['stop_loss'] = entry * 0.98 # 2% below entry repairs.append(f"SL {sl:.4f}->{pos['stop_loss']:.4f}") # Fix SL below entry for SHORT elif direction == 'SHORT' and sl <= entry: pos['stop_loss'] = entry * 1.02 # 2% above entry repairs.append(f"SL {sl:.4f}->{pos['stop_loss']:.4f}") # Fix TP1 below entry for LONG if direction == 'LONG' and tp1 <= entry: pos['take_profit1'] = entry * 1.02 # 2% above entry repairs.append(f"TP1 {tp1:.4f}->{pos['take_profit1']:.4f}") # Fix TP1 above entry for SHORT elif direction == 'SHORT' and tp1 >= entry: pos['take_profit1'] = entry * 0.98 # 2% below entry repairs.append(f"TP1 {tp1:.4f}->{pos['take_profit1']:.4f}") # Fix TP1 == SL if abs(pos.get('take_profit1', 0) - pos.get('stop_loss', 0)) < 0.0001: if direction == 'LONG': pos['take_profit1'] = entry * 1.02 pos['stop_loss'] = entry * 0.98 else: pos['take_profit1'] = entry * 0.98 pos['stop_loss'] = entry * 1.02 repairs.append("TP1==SL fixed") if repairs: print(f"[POSITION REPAIR] {pair}: {', '.join(repairs)}") stats = self.position_manager.get_stats() # === BIG BRAIN: Check positions for abort signals (throttled: every 30s per position) === if open_positions and getattr(self, 'bot_engaged', False): for pos in open_positions: # Throttle: only check every 30 seconds per position last_abort_check = pos.get('_last_abort_check', 0) if now - last_abort_check < 30: continue pos['_last_abort_check'] = now abort_check = self.check_position_abort_signal(pos) if abort_check.get('abort'): self.log_activity( f"[ABORT SIGNAL] {pos['pair']}: {abort_check['reason']} " f"(confidence: {abort_check['confidence']}%)", "WARN" ) # Optionally auto-close based on confidence if abort_check['confidence'] >= 75 and getattr(self, 'auto_abort_enabled', False): current_price = abort_check.get('current_price', pos.get('current_price', 0)) pnl = self.position_manager.close_position( pos['id'], current_price, "ABORT_SIGNAL" ) self.log_activity( f"[AUTO ABORT] {pos['pair']} closed at ${current_price:.4f} (PnL: ${pnl:+.2f})", "TRADE" ) if hasattr(self, 'sound_manager'): self.sound_manager.play('trade_exit') # === BOT-FATHER: Intelligent position monitoring === if open_positions and hasattr(self, 'bot_father'): try: self.bot_father.monitor_all_positions() except Exception as e: print(f"[BOT-FATHER ERROR] {e}") daily_color = GREEN if stats['daily_pnl'] >= 0 else RED weekly_color = GREEN if stats['weekly_pnl'] >= 0 else RED total_color = GREEN if stats['total_pnl'] >= 0 else RED unreal_color = GREEN if stats['unrealized_pnl'] >= 0 else RED # Update P&L labels (now in Assets tab) - check if they exist if hasattr(self, 'daily_pnl_lbl'): self.daily_pnl_lbl.text = f"[b]Daily: ${stats['daily_pnl']:+.2f}[/b]" self.daily_pnl_lbl.color = daily_color self.daily_pnl_lbl.markup = True if hasattr(self, 'weekly_pnl_lbl'): self.weekly_pnl_lbl.text = f"[b]Weekly: ${stats['weekly_pnl']:+.2f}[/b]" self.weekly_pnl_lbl.color = weekly_color self.weekly_pnl_lbl.markup = True if hasattr(self, 'total_pnl_lbl'): self.total_pnl_lbl.text = f"[b]Total: ${stats['total_pnl']:+.2f}[/b]" self.total_pnl_lbl.color = total_color self.total_pnl_lbl.markup = True if hasattr(self, 'unrealized_pnl_lbl'): self.unrealized_pnl_lbl.text = f"[b]Unrealized: ${stats['unrealized_pnl']:+.2f}[/b]" self.unrealized_pnl_lbl.color = unreal_color self.unrealized_pnl_lbl.markup = True # Update notional asset value and leverage display if hasattr(self, 'notional_value_lbl'): total_position_value = sum(p.get('position_value', 0) for p in open_positions) notional_value = total_position_value * self.leverage self.notional_value_lbl.text = f"[b]Notional: ${notional_value:,.0f}[/b]" if hasattr(self, 'leverage_display_lbl'): self.leverage_display_lbl.text = f"[b]Leverage: {self.leverage}X[/b]" # Update Daily P&L Summary card (in Positions tab) - European format if hasattr(self, 'daily_realized_lbl'): realized_color = GREEN if stats['daily_pnl'] >= 0 else RED realized_sign = '+' if stats['daily_pnl'] >= 0 else '' realized_abs = abs(stats['daily_pnl']) realized_formatted = f"{realized_abs:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") self.daily_realized_lbl.text = f"[b]Realized: {realized_sign}${realized_formatted}[/b]" self.daily_realized_lbl.color = realized_color if hasattr(self, 'daily_unrealized_lbl'): unrealized_total = sum(p.get('unrealized_pnl', 0) for p in open_positions) unrealized_color = GREEN if unrealized_total >= 0 else RED unrealized_sign = '+' if unrealized_total >= 0 else '' unrealized_abs = abs(unrealized_total) unrealized_formatted = f"{unrealized_abs:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") self.daily_unrealized_lbl.text = f"[b]Unrealized: {unrealized_sign}${unrealized_formatted}[/b]" self.daily_unrealized_lbl.color = unrealized_color # Update Today's Net P&L (Realized + Unrealized) if hasattr(self, 'daily_net_lbl'): realized_pnl = stats.get('daily_pnl', 0) unrealized_pnl = sum(p.get('unrealized_pnl', 0) for p in open_positions) net_pnl = realized_pnl + unrealized_pnl net_color = GREEN if net_pnl >= 0 else RED net_sign = '+' if net_pnl >= 0 else '' net_abs = abs(net_pnl) net_formatted = f"{net_abs:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") self.daily_net_lbl.text = f"[b]Today's Net: {net_sign}${net_formatted}[/b]" self.daily_net_lbl.color = net_color # Update last reset info if hasattr(self, 'pnl_reset_lbl'): try: last_daily = getattr(self.position_manager, '_last_daily_reset', None) if last_daily: from datetime import datetime, timezone last_dt = datetime.fromisoformat(last_daily) now = datetime.now(timezone.utc) days_ago = (now.date() - last_dt.date()).days if days_ago == 0: reset_text = "Today" elif days_ago == 1: reset_text = "Yesterday" else: reset_text = f"{days_ago} days ago" self.pnl_reset_lbl.text = f"[color=888888]Last Reset: {reset_text}[/color]" except: pass self.active_pos_header.text = f"[b]ACTIVE POSITIONS ({len(open_positions)}/{self.max_simultaneous})[/b]" if not open_positions: self.positions_container.add_widget(Label(text="No active positions", color=GRAY, font_size=sp(12), size_hint_y=None, height=dp(50))) else: for pos in open_positions: card = BorderedCard(size_hint_y=None, height=dp(240)) card.orientation = 'vertical' card.padding = dp(8) card.spacing = dp(2) pnl = pos.get('unrealized_pnl', 0) # VALIDATION: PnL must be reasonable (not $425k on a $200 position) position_value = pos.get('position_value', 0) if position_value > 0 and abs(pnl) > position_value * 10: # PnL > 10x position value = corrupted print(f"[PNL CORRUPTION] {pos['pair']}: PnL ${pnl:.2f} vs Value ${position_value:.2f}, resetting") pnl = 0 # Reset corrupted PnL pos['unrealized_pnl'] = 0 pnl_color = GREEN if pnl >= 0 else RED current_price = self.market_data.get(pos['pair'], {}).get('price', pos['entry_price']) # VALIDATION: Current price must be reasonable relative to entry entry = pos.get('entry_price', 0) if entry > 0 and current_price > 0: price_change = abs(current_price - entry) / entry if price_change > 0.5: # >50% change = likely corrupted (was 200%) print(f"[PRICE CORRUPTION] {pos['pair']}: Current ${current_price:.4f} vs Entry ${entry:.4f} ({price_change:.0%} change), using entry") current_price = entry # Fall back to entry price # Also reset the corrupted distance percentages pos['entry_distance_pct'] = 0 pos['tp1_distance_pct'] = abs((pos['take_profit1'] - entry) / entry * 100) entry_dist = pos.get('entry_distance_pct', 0) entry_arrow = "+" if entry_dist >= 0 else "-" tp1_dist = pos.get('tp1_distance_pct', 0) # Count DOWN to target: 100% at entry, 0% at TP1 # If tp1_dist is 15% away, show "85,0% left to reach target" if tp1_dist > 0: pct_left = 100 - tp1_dist # Count down to target if pct_left < 0: pct_left = 0 # Cap at 0% # Format with comma as decimal separator (European style) tp1_text = f"{pct_left:.1f}".replace(".", ",") + " % left to reach target" else: tp1_text = "AT TP1!" dynamic_indicator = " [DYN]" if pos.get('dynamic_tp_active') else "" direction_color = GREEN if pos['direction'] == 'LONG' else RED direction_text = "UP" if pos['direction'] == 'LONG' else "DN" # Convert color tuple to hex for markup def rgb_to_hex(rgb): return f"{int(rgb[0]*255):02x}{int(rgb[1]*255):02x}{int(rgb[2]*255):02x}" direction_color_hex = rgb_to_hex(direction_color) # Get grade for display grade = pos.get('grade', 'C') grade_colors = { 'A+': '00FF00', 'A': '00FF00', 'A-': '00DD00', 'B+': '88FF00', 'B': '88FF00', 'B-': '88DD00', 'C+': 'FFFF00', 'C': 'FFFF00', 'C-': 'DDDD00', 'D+': 'FF8800', 'D': 'FF8800', 'D-': 'DD6600', 'F': 'FF0000' } grade_color_hex = grade_colors.get(grade, 'FFFFFF') header = Label( text=f"[b]{pos['pair']}[/b] | [color={direction_color_hex}]{pos['direction']}[/color] | Grade: [color={grade_color_hex}][b]{grade}[/b][/color]", markup=True, color=GOLD, font_size=sp(14), size_hint_y=None, height=dp(28) ) card.add_widget(header) content_box = BoxLayout(orientation='vertical', size_hint_y=None, height=dp(140), spacing=dp(2)) # Format P&L in European style: $1.234,56 (period=thousands, comma=decimal) pnl_abs = abs(pnl) pnl_formatted = f"{pnl_abs:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") pnl_sign = '+' if pnl >= 0 else '-' pnl_label = Label( text=f"[b]{pnl_sign}${pnl_formatted}[/b]", markup=True, color=pnl_color, font_size=sp(16), size_hint_y=None, height=dp(32) ) content_box.add_widget(pnl_label) # Use format_price for adaptive decimal places (more digits for small prices) pair_name = pos.get('pair', '') # Current price ABOVE entry line, with BOLD text current_price_formatted = format_price(current_price, pair_name) entry_price_formatted = format_price(pos['entry_price'], pair_name) content_box.add_widget(Label( text=f"Current: [b]{current_price_formatted}[/b] | Entry: {entry_price_formatted} | {entry_arrow}{abs(entry_dist):.2f}%", markup=True, color=WHITE, font_size=sp(12), size_hint_y=None, height=dp(24) )) # Calculate TP1 percentage based on direction and current price # If heading toward SL (against trade direction), show negative tp1_price = pos.get('take_profit1', 0) entry_price = pos.get('entry_price', 0) direction = pos.get('direction', 'LONG') sl_price = pos.get('stop_loss', 0) if direction == 'LONG': # For LONG: Entry < TP1, Entry > SL if current_price < entry_price: # Price below entry - heading toward SL # Calculate distance from entry to current as negative percentage toward SL total_entry_to_sl = abs(entry_price - sl_price) if total_entry_to_sl > 0: distance_from_entry = abs(entry_price - current_price) # Count down from 100% at entry to 0% at SL pct_left = max(0, 100 - (distance_from_entry / total_entry_to_sl * 100)) tp1_text = f"-{pct_left:.1f}%".replace(".", ",") + " (toward SL)" else: tp1_text = "SL ERROR" else: # Price above entry - normal TP1 countdown if tp1_dist > 0: pct_left = 100 - tp1_dist if pct_left < 0: pct_left = 0 tp1_text = f"{pct_left:.1f}".replace(".", ",") + "% left to target" else: tp1_text = "AT TP1!" else: # For SHORT: Entry > TP1, Entry < SL if current_price > entry_price: # Price above entry - heading toward SL total_entry_to_sl = abs(sl_price - entry_price) if total_entry_to_sl > 0: distance_from_entry = abs(current_price - entry_price) # Count down from 100% at entry to 0% at SL pct_left = max(0, 100 - (distance_from_entry / total_entry_to_sl * 100)) tp1_text = f"-{pct_left:.1f}%".replace(".", ",") + " (toward SL)" else: tp1_text = "SL ERROR" else: # Price below entry - normal TP1 countdown if tp1_dist > 0: pct_left = 100 - tp1_dist if pct_left < 0: pct_left = 0 tp1_text = f"{pct_left:.1f}".replace(".", ",") + "% left to target" else: tp1_text = "AT TP1!" tp1_status = "DONE" if pos.get('tp1_hit') else "PENDING" tp1_color_widget = GREEN if pos.get('tp1_hit') else AMBER content_box.add_widget(Label( text=f"TP1: {format_price(tp1_price, pair_name)} [{tp1_status}] {tp1_text}", markup=True, color=tp1_color_widget, font_size=sp(12), size_hint_y=None, height=dp(24) )) # TP2 on separate line tp2_price = pos.get('take_profit2', 0) tp2_status = "DONE" if pos.get('tp2_hit') else "PENDING" tp2_color = GREEN if pos.get('tp2_hit') else AMBER content_box.add_widget(Label( text=f"TP2: {format_price(tp2_price, pair_name)} [{tp2_status}]", markup=True, color=tp2_color, font_size=sp(12), size_hint_y=None, height=dp(24) )) # SL line with percentage from entry to SL when price is against trade qty_remaining = pos['quantity'] - pos.get('closed_quantity', 0) trail_price = pos.get('trailing_stop', 0) trail_info = f"Trail: {format_price(trail_price, pair_name)}" if trail_price else "No trail" # Calculate SL percentage when price is against trade if direction == 'LONG' and current_price < entry_price: # LONG: Price below entry - calculate progress toward SL total_entry_to_sl = abs(entry_price - sl_price) if total_entry_to_sl > 0: distance_from_entry = abs(entry_price - current_price) # 100% at entry, 0% at SL sl_pct = max(0, 100 - (distance_from_entry / total_entry_to_sl * 100)) sl_text = f" ({sl_pct:.1f}%" + " left to SL)" else: sl_text = "" elif direction == 'SHORT' and current_price > entry_price: # SHORT: Price above entry - calculate progress toward SL total_entry_to_sl = abs(sl_price - entry_price) if total_entry_to_sl > 0: distance_from_entry = abs(current_price - entry_price) # 100% at entry, 0% at SL sl_pct = max(0, 100 - (distance_from_entry / total_entry_to_sl * 100)) sl_text = f" ({sl_pct:.1f}%" + " left to SL)" else: sl_text = "" else: sl_text = "" # === DUAL ENTRY DISPLAY === dual_entry_text = "" if pos.get('dual_entry_part'): part = pos.get('dual_entry_part') if part == 'first': # First entry - show countdown to Stage 2 trigger trigger_price = pos.get('dual_entry_trigger', 0) if trigger_price > 0 and entry_price > 0: direction = pos.get('direction', 'LONG') if direction == 'LONG': # For LONG: trigger is above entry total_distance = trigger_price - entry_price current_distance = current_price - entry_price if current_distance >= total_distance: dual_entry_text = " | [color=00FF00]Stage 2: READY[/color]" elif current_price < entry_price: pct_there = max(0, 100 - (abs(current_price - entry_price) / total_distance * 100)) dual_entry_text = f" | Stage 2: {pct_there:.1f}%".replace(".", ",") else: pct_there = min(100, (current_distance / total_distance * 100)) pct_left = max(0, 100 - pct_there) dual_entry_text = f" | Stage 2: {pct_left:.1f}%".replace(".", ",") else: # For SHORT: trigger is below entry total_distance = entry_price - trigger_price current_distance = entry_price - current_price if current_distance >= total_distance: dual_entry_text = " | [color=00FF00]Stage 2: READY[/color]" elif current_price > entry_price: pct_there = max(0, 100 - (abs(current_price - entry_price) / total_distance * 100)) dual_entry_text = f" | Stage 2: {pct_there:.1f}%".replace(".", ",") else: pct_there = min(100, (current_distance / total_distance * 100)) pct_left = max(0, 100 - pct_there) dual_entry_text = f" | Stage 2: {pct_left:.1f}%".replace(".", ",") else: dual_entry_text = " | Stage 2: Pending" elif part == 'second': dual_entry_text = " | [color=00FF00]Stage 2: FILLED[/color]" content_box.add_widget(Label( text=f"SL: {format_price(pos['stop_loss'], pair_name)}{sl_text} | {trail_info} | Qty: {qty_remaining:.2f}{dual_entry_text}", markup=True, color=GRAY, font_size=sp(11), size_hint_y=None, height=dp(22) )) card.add_widget(content_box) close_btn = Button( text="[b]CLOSE POSITION[/b]", markup=True, background_color=(0, 0, 0, 0), # Transparent - use canvas only color=WHITE, font_size=sp(11), size_hint_y=None, height=dp(36), size_hint_x=0.6, pos_hint={'center_x': 0.5} ) with close_btn.canvas.before: Color(*DARK_RED) close_btn.rect = RoundedRectangle(pos=close_btn.pos, size=close_btn.size, radius=[dp(12)]) close_btn.bind(pos=lambda btn, val: setattr(btn.rect, 'pos', val)) close_btn.bind(size=lambda btn, val: setattr(btn.rect, 'size', val)) close_btn.bind(on_press=lambda x, pid=pos['id']: self.close_position_manual(pid)) card.add_widget(close_btn) self.positions_container.add_widget(card) self.positions_container.height = max(len(open_positions) * dp(250), dp(100)) # Shorter format to prevent overflow: T=Total, W=Win%, O=Open/Max self.stats_lbl.text = f"T:{stats['total_trades']} W:{stats['win_rate']:.0f}% O:{stats['open_positions']}/{self.max_simultaneous}" def update_market_data(self, dt=None): def fetch(): for pair in TOP_PAIRS[:5]: try: price = self.binance.get_ticker_price(pair) # VALIDATION: Price must be reasonable (within 50% of cached price if exists) if price > 0: cached = self.market_data.get(pair, {}).get('price', 0) if cached > 0: change_pct = abs(price - cached) / cached if change_pct > 0.5: # >50% change = suspicious print(f"[PRICE WARNING] {pair}: {price} is {change_pct:.1%} from cached {cached}, skipping") continue self.market_data[pair] = {'price': price, 'timestamp': time.time()} except Exception as e: print(f"[MARKET DATA ERROR] {pair}: {e}") threading.Thread(target=fetch, daemon=True).start() def auto_refresh_setups(self, dt): """Auto-refresh setups every 2 minutes, but skip if positions are full.""" # Check if positions are full - skip scan to save API calls open_count = len([p for p in self.position_manager.positions if p['status'] == 'OPEN']) if open_count >= self.max_simultaneous: if not getattr(self, 'silent_mode', False): print(f"[AUTO REFRESH] Skipping scan - positions full ({open_count}/{self.max_simultaneous})") return if self.setup_search_paused: return if not self.auto_refresh: return if self.is_scanning: return now = time.time() time_since_last = now - self.last_auto_scan should_scan = False if time_since_last >= self.auto_scan_interval: should_scan = True scan_reason = "2-min interval" elif len(self.current_setups) == 0 and time_since_last >= self.no_setups_retry_delay: should_scan = True scan_reason = "no setups retry" if should_scan: self.is_scanning = True self.last_auto_scan = now if not getattr(self, 'silent_mode', False): self.log_activity(f"[b]AUTO REFRESH:[/b] {scan_reason} scan") self._silent_market_scan() def _silent_market_scan(self): pairs_to_scan = ['BTCUSDC'] if DEBUG_BTC_ONLY else self.get_scan_pairs() with self._filter_lock: all_limits_for_scan = self.all_limits_off def scan_pair_silent(pair): try: # Use multi-timeframe scanning when available tf_data = self.binance.fetch_multi_timeframe_data(pair) # Check if we got ALL 6 timeframes - required for trading all_timeframes = ['3m', '5m', '15m', '1h', '2h', '4h'] has_all_timeframes = all(tf_data.get(tf) and tf_data[tf].get('prices') and len(tf_data[tf]['prices']) >= 30 for tf in all_timeframes) if has_all_timeframes: # Calculate exhaustion score for reversal detection prices_15m = tf_data['15m']['prices'] volumes_15m = tf_data['15m']['volumes'] exhaustion = calculate_momentum_exhaustion(prices_15m, volumes_15m) exhaustion_score = exhaustion.get('score', 50) # Use multi-timeframe scan setup = scan_multi_timeframe( tf_data, pair, no_filter=(self.test_mode or all_limits_for_scan), min_grade=self.min_grade_filter, scalping_mode=getattr(self, 'scalping_mode_enabled', False), scalp_target_pct=getattr(self, 'scalping_profit_target', 0.004), scalp_stop_pct=getattr(self, 'scalping_stop_loss', 0.003), lower_tf_priority=getattr(self, 'lower_tf_priority', False), min_consensus=getattr(self, 'min_consensus', 4), exhaustion_score=exhaustion_score ) if setup and setup.get('detected'): current_price = tf_data['15m']['prices'][-1] # === SCALP DETECTION for auto-scan === scalp_result = detect_scalp_setup(tf_data, pair, dxy_change=0, fear_greed_index=50) if scalp_result['is_scalp']: setup['is_scalp'] = True setup['scalp_confidence'] = scalp_result['confidence'] setup['scalp_direction'] = scalp_result['direction'] setup['scalp_score'] = scalp_result['score'] print(f"[AUTO_SCAN SCALP] {pair} {scalp_result['direction']} {scalp_result['confidence']}% confidence") else: setup['is_scalp'] = False setup['scalp_confidence'] = 0 return { 'pair': pair, 'price': current_price, 'setup': setup, 'multi_tf': True } # Fallback to single timeframe scan klines = self.binance.get_klines(pair, '1h', 50) if not klines or not isinstance(klines, list) or len(klines) == 0: return None if not isinstance(klines[0], (list, tuple)) or len(klines[0]) < 6: return None prices = [float(k[4]) for k in klines if isinstance(k, (list, tuple)) and len(k) > 4] volumes = [float(k[5]) for k in klines if isinstance(k, (list, tuple)) and len(k) > 5] if prices and len(prices) >= 10: setup = scan_setups(prices, volumes, pair, no_filter=(self.test_mode or all_limits_for_scan), min_grade=self.min_grade_filter, scalping_mode=getattr(self, 'scalping_mode_enabled', False), scalp_target_pct=getattr(self, 'scalping_profit_target', 0.004), scalp_stop_pct=getattr(self, 'scalping_stop_loss', 0.003)) if setup and setup.get('detected') and setup.get('entry', 0) == 0: self.log_activity(f"[b]DEBUG SCAN:[/b] {pair} entry=0! setup_type={setup.get('setup_type')}, score={setup.get('score')}", "WARN") return { 'pair': pair, 'price': prices[-1], 'setup': setup, 'multi_tf': False } except Exception: pass return None def scan_worker(): all_setups = [] BATCH_SIZE = 30 total_pairs = len(pairs_to_scan) batches = [pairs_to_scan[i:i+BATCH_SIZE] for i in range(0, total_pairs, BATCH_SIZE)] for batch_num, batch in enumerate(batches, 1): # Use existing thread pool instead of creating new one executor = self._thread_pool future_to_pair = {executor.submit(scan_pair_silent, pair): pair for pair in batch} for future in as_completed(future_to_pair): result = future.result() if result: self.market_data[result['pair']] = {'price': result['price'], 'timestamp': time.time()} if result['setup'] and result['setup'].get('detected'): all_setups.append(result['setup']) if batch_num < len(batches): time.sleep(0.3) from kivy.clock import Clock Clock.schedule_once(lambda dt, r=all_setups: self._auto_scan_complete(r), 0) threading.Thread(target=scan_worker, daemon=True).start() def _auto_scan_complete(self, results): self.is_scanning = False results.sort(key=lambda x: x.get('score', 0), reverse=True) if results: first = results[0] self.log_activity(f"[b]DEBUG SCAN COMPLETE:[/b] {first.get('pair')} entry={first.get('entry',0)}, sl={first.get('stop_loss',0)}, tp={first.get('take_profit1',0)}", "WARN") if results: for setup in results: self.log_setup_found(setup) if len(results) != len(self.current_setups): self.current_setups = results self.setups_container.clear_widgets() if results: self.setups_status_lbl.text = "Auto-scan complete" self.setups_status_lbl.color = GREEN self.found_setups_lbl.text = f"[b]FOUND {len(results)} SETUP{'S' if len(results) > 1 else ''}[/b]" for setup in results: card = SetupCard(setup, on_trade_callback=self.manual_trade) self.setups_container.add_widget(card) best = results[0] self.update_best_setup_display(best) if not getattr(self, 'silent_mode', False): self.log_activity(f"Auto-scan: {len(results)} setup(s)") if self.bot_engaged: if not getattr(self, 'silent_mode', False): self.log_activity("[b]AUTO-TRADE:[/b] Bot engaged, checking for entry opportunities...", "TRADE") Clock.schedule_once(lambda dt: self.check_and_enter_trade(), 0.5) else: self.setups_status_lbl.text = "No setups found" self.setups_status_lbl.color = GRAY self.found_setups_lbl.text = "" if hasattr(self, 'best_setup_content'): self.best_setup_content.text = "No setups available." self.best_setup_content.color = GRAY else: changed = False if len(results) == len(self.current_setups): for i, (new, old) in enumerate(zip(results, self.current_setups)): if new.get('score') != old.get('score') or new.get('price') != old.get('price'): changed = True break else: changed = True if self.bot_engaged and results: open_count = len([p for p in self.position_manager.positions if p['status'] == 'OPEN']) if open_count < self.max_simultaneous: if not getattr(self, 'silent_mode', False): self.log_activity("[b]AUTO-TRADE:[/b] Checking entry opportunities...", "TRADE") Clock.schedule_once(lambda dt: self.check_and_enter_trade(), 0.5) if changed: self.current_setups = results self.setups_container.clear_widgets() for setup in results: card = SetupCard(setup, on_trade_callback=self.manual_trade) self.setups_container.add_widget(card) if results: best = results[0] self.update_best_setup_display(best) def log_activity(self, message, message_type="INFO"): import threading from kivy.clock import Clock if threading.current_thread() != threading.main_thread(): def do_log(dt): self._log_activity_main(message, message_type) Clock.schedule_once(do_log, 0) else: self._log_activity_main(message, message_type) def _log_activity_main(self, message, message_type="INFO"): from datetime import datetime # === COMPREHENSIVE AUTO LOG === if hasattr(self, 'auto_log_file'): try: ts_full = datetime.now().strftime('%H:%M:%S.%f')[:-3] with open(self.auto_log_file, 'a') as f: f.write(f"[{ts_full}] [{message_type:10}] {message}\n") except: pass # Safety check - bot_log may not exist during early initialization if not hasattr(self, 'bot_log') or self.bot_log is None: print(f"[LOG_EARLY] {message}") return timestamp = datetime.now().strftime("%H:%M:%S") color_map = { "INFO": WHITE, "TRADE": GREEN, "ERROR": RED, "WARN": AMBER, "SETUP": CYAN, "SUCCESS": (0.2, 0.8, 0.2, 1), "PROFIT": (0.4, 1.0, 0.4, 1), "LOSS": (1.0, 0.4, 0.4, 1), } color = color_map.get(message_type, WHITE) log_entry = f"[{timestamp}] {message}" lbl = Label( text=log_entry, markup=True, color=color, font_size=sp(12), size_hint_y=None, height=dp(32), halign='left', valign='middle', padding=(dp(4), dp(2)) ) lbl.bind(width=lambda lbl, w: lbl.setter('text_size')(lbl, (w, None))) lbl.bind(texture_size=lambda lbl, s: setattr(lbl, 'height', max(dp(28), s[1] + dp(8)))) if len(self.bot_log.children) > 0 and len(self.bot_log.children) % 5 == 0: spacer = Label( text="", size_hint_y=None, height=dp(4), color=(0.2, 0.2, 0.2, 1) ) self.bot_log.add_widget(spacer, index=len(self.bot_log.children)) self.bot_log.add_widget(lbl, index=len(self.bot_log.children)) self.bot_log.height = sum(c.height for c in self.bot_log.children) + dp(10) while len(self.bot_log.children) > 40: self.bot_log.remove_widget(self.bot_log.children[0]) self.bot_log.height = sum(c.height for c in self.bot_log.children) + dp(10) print(f"[BOT LOG] [{message_type}] {log_entry}") def toggle_auto_refresh(self, instance): """Toggle auto-refresh mode.""" # Confirmation new_state = not self.auto_refresh state_text = "ON" if new_state else "OFF" def _do_toggle(): self.auto_refresh = new_state self.update_auto_refresh_button() self.log_activity(f"Auto refresh {state_text}") self.show_confirmation_dialog( "AUTO REFRESH", f"Turn auto refresh {state_text}?", _do_toggle ) return self.auto_refresh = not self.auto_refresh instance.text = "[b]AUTO[/b]" if self.auto_refresh else "[b]MANUAL[/b]" instance.set_bg_color(GREEN if self.auto_refresh else GRAY) self.log_activity(f"Auto-refresh: {'ON' if self.auto_refresh else 'OFF'}") def toggle_test_mode(self, instance): """Toggle test mode (shows all setups regardless of grade).""" self.test_mode = not self.test_mode if self.test_mode: self.min_grade_filter = "F" instance.text = "[b]TEST[/b]" instance.set_bg_color(CYAN) self.log_activity("Test mode: ON (all setups, all grades F to A+)") else: self.min_grade_filter = "F" instance.text = "[b]FILTER[/b]" instance.set_bg_color(GRAY) self.log_activity("Test mode: OFF (filtered to F and above - ALL GRADES)") if hasattr(self, 'min_grade_btn'): self.min_grade_btn.text = f"[b]{self.min_grade_filter}[/b]" self.current_setups = [] self.setups_container.clear_widgets() self.setups_status_lbl.text = "Mode changed - Press REFRESH" self.setups_status_lbl.color = AMBER def toggle_regime_filter(self, instance=None): """Toggle regime filter.""" with self._filter_lock: self.regime_filter_enabled = not self.regime_filter_enabled # Only update button UI if called from a button (not MODES menu) if instance and hasattr(instance, 'text') and hasattr(instance, 'bg_color'): instance.text = "[b]ON[/b]" if self.regime_filter_enabled else "[b]OFF[/b]" instance.bg_color = GREEN if self.regime_filter_enabled else GRAY self.log_activity(f"Regime filter: {'ENABLED (skip choppy markets)' if self.regime_filter_enabled else 'DISABLED (trade all conditions)'}") def toggle_scalping_mode(self, instance=None): """Toggle scalping mode for sideways markets.""" self.scalping_mode_enabled = not getattr(self, 'scalping_mode_enabled', True) # Only update instance UI if it's the scalping toggle button (has specific attributes) if instance and hasattr(instance, 'text') and hasattr(instance, 'bg_color'): instance.text = "[b]ON[/b]" if self.scalping_mode_enabled else "[b]OFF[/b]" instance.bg_color = GREEN if self.scalping_mode_enabled else GRAY # Update main UI labels if they exist if hasattr(self, 'scalping_status_lbl'): if self.scalping_mode_enabled: self.scalping_status_lbl.text = "Active in sideways markets | VWAP + Order Book" else: self.scalping_status_lbl.text = "Disabled - Using standard swing trading" if hasattr(self, 'scalping_details_lbl'): if self.scalping_mode_enabled: self.scalping_details_lbl.text = "Quick 0.4% targets | 5min max hold | Micro-structure entry" else: self.scalping_details_lbl.text = "Normal R/R targets | Standard hold times" if self.scalping_mode_enabled: self.log_activity("[b]SCALPING MODE:[/b] ENABLED - Quick trades in sideways markets", "SUCCESS") else: self.log_activity("[b]SCALPING MODE:[/b] DISABLED - Standard trading mode") # Save settings self.save_all_settings() # ============================================ # MONEY MAKER MODE - GUARANTEED PROFIT ENGINE # ============================================ def _show_money_maker_confirmation(self, instance=None): """Show confirmation dialog for Money Maker mode with target selector.""" import time if self.money_maker_mode: # Show status instead if already active self.show_money_maker_status() return # Create fresh content container content = BoxLayout(orientation='vertical', spacing=dp(8), padding=dp(16)) # Title title_lbl = Label( text="[b]🚜 ACTIVATE MONEY BULLDOZER?[/b]", markup=True, color=GOLD, font_size=sp(16), size_hint_y=None, height=dp(36) ) content.add_widget(title_lbl) # Description desc_lbl = Label( text="[b]AGGRESSIVE trading mode to capture profit in 6 hours.[/b]", markup=True, color=WHITE, font_size=sp(11), size_hint_y=None, height=dp(28), halign='center' ) content.add_widget(desc_lbl) # Target selector label select_lbl = Label( text="[b]SELECT TARGET:[/b]", markup=True, color=CYAN, font_size=sp(12), size_hint_y=None, height=dp(24) ) content.add_widget(select_lbl) # Target selector buttons container target_box = BoxLayout(size_hint_y=None, height=dp(50), spacing=dp(12), padding=dp(4)) # Store selected target on self to avoid closure issues self._mm_selected_target = getattr(self, 'money_maker_target_preset', 10.0) # Create buttons with references stored on self self._mm_btn5 = StyledButton( text="[b]$5[/b]", markup=True, bg_color=GOLD if self._mm_selected_target == 5.0 else CARD_BG, text_color=BLACK if self._mm_selected_target == 5.0 else GRAY, font_size=sp(14), radius=8 ) self._mm_btn10 = StyledButton( text="[b]$10[/b]", markup=True, bg_color=GOLD if self._mm_selected_target == 10.0 else CARD_BG, text_color=BLACK if self._mm_selected_target == 10.0 else GRAY, font_size=sp(14), radius=8 ) # Settings display label - store reference to update it self._mm_settings_lbl = Label( text=f"Target: ${self._mm_selected_target:.0f} | Max Loss: ${self._mm_selected_target * 2:.0f} | R/R: 1:1.5 | 5x Leverage", markup=True, color=AMBER, font_size=sp(11), size_hint_y=None, height=dp(28) ) def update_settings_display(): target = self._mm_selected_target max_loss = target * 2 self._mm_settings_lbl.text = f"Target: ${target:.0f} | Max Loss: ${max_loss:.0f} | R/R: 1:1.5 | 5x Leverage" def select_target_5(btn): self._mm_selected_target = 5.0 self._mm_btn5.bg_color = GOLD self._mm_btn5.text_color = BLACK self._mm_btn10.bg_color = CARD_BG self._mm_btn10.text_color = GRAY update_settings_display() def select_target_10(btn): self._mm_selected_target = 10.0 self._mm_btn10.bg_color = GOLD self._mm_btn10.text_color = BLACK self._mm_btn5.bg_color = CARD_BG self._mm_btn5.text_color = GRAY update_settings_display() self._mm_btn5.bind(on_press=select_target_5) self._mm_btn10.bind(on_press=select_target_10) target_box.add_widget(self._mm_btn5) target_box.add_widget(self._mm_btn10) content.add_widget(target_box) # Settings display content.add_widget(self._mm_settings_lbl) # Warning warn_lbl = Label( text="[b]⚠️ WARNING:[/b] High frequency, tight stops. Trade at your own risk.", markup=True, color=RED, font_size=sp(10), size_hint_y=None, height=dp(28) ) content.add_widget(warn_lbl) # Buttons container buttons = BoxLayout(size_hint_y=None, height=dp(48), spacing=dp(12)) # Store popup reference on self to avoid closure issues self._mm_popup = None def on_cancel(btn): if self._mm_popup: self._mm_popup.dismiss() self._mm_popup = None def on_activate(btn): target = self._mm_selected_target if self._mm_popup: self._mm_popup.dismiss() self._mm_popup = None self._start_money_maker_session_with_target(target) cancel_btn = StyledButton( text="[b]CANCEL[/b]", markup=True, bg_color=GRAY, text_color=WHITE, font_size=sp(12), radius=8 ) cancel_btn.bind(on_press=on_cancel) buttons.add_widget(cancel_btn) activate_btn = StyledButton( text="[b]💰 ACTIVATE[/b]", markup=True, bg_color=DARK_GREEN, text_color=WHITE, font_size=sp(12), radius=8 ) activate_btn.bind(on_press=on_activate) buttons.add_widget(activate_btn) content.add_widget(buttons) # Create popup with fresh content self._mm_popup = Popup( title='[b]MONEY MAKER MODE[/b]', content=content, size_hint=(0.9, 0.6), background_color=DARK_BG, title_color=GOLD, auto_dismiss=True ) self._mm_popup.open() def _on_mm_activate_btn(self, instance): """Handle Money Maker card activate/stop button.""" if self.money_maker_mode: # Stop the session self._stop_money_maker_session("Manual Stop") # Update UI (only if widgets exist - card may be removed) if hasattr(self, 'mm_activate_btn'): instance.text = "[b]💰 ACTIVATE[/b]" instance.bg_color = DARK_GREEN if hasattr(self, 'mm_status_lbl'): self.mm_status_lbl.text = "[b]OFF[/b]" self.mm_status_lbl.color = GRAY else: # Show confirmation to start self._show_money_maker_confirmation() def _update_mm_card_ui(self): """Update Money Maker card UI based on current state.""" if hasattr(self, 'mm_activate_btn'): if self.money_maker_mode: self.mm_activate_btn.text = "[b]⏹ STOP[/b]" self.mm_activate_btn.bg_color = DARK_RED else: self.mm_activate_btn.text = "[b]💰 ACTIVATE[/b]" self.mm_activate_btn.bg_color = DARK_GREEN if hasattr(self, 'mm_status_lbl'): if self.money_maker_mode: self.mm_status_lbl.text = "[b]ACTIVE[/b]" self.mm_status_lbl.color = GREEN else: self.mm_status_lbl.text = "[b]OFF[/b]" self.mm_status_lbl.color = GRAY if hasattr(self, 'mm_target_lbl'): self.mm_target_lbl.text = f"Target: ${getattr(self, 'money_maker_target_preset', 10):.0f}" def _start_money_maker_session_with_target(self, target_amount): """Start Money Maker session with selected target amount ($5 or $10).""" import time # Set the target and calculate max loss (2x target) self.money_maker_target_preset = target_amount self.money_maker_target_profit = target_amount self.money_maker_max_loss = target_amount * 2 # Now call the main start function self._start_money_maker_session() def _start_money_maker_session(self): """Initialize a Money Maker trading session.""" import time self.money_maker_mode = True self.money_maker_session_start = time.time() self.money_maker_session_profit = 0.0 self.money_maker_trade_count = 0 self.money_maker_active_trades = [] # Generate unique session ID for tracking self._generate_mm_session_id() # OVERRIDE settings for aggressive profit hunting self._mm_save_original_settings() # Apply Money Maker settings - BULLDOZER MODE self.min_rr_ratio = 1.0 # Minimum R/R - allow almost any setup self.volume_threshold = 5000000 # HIGH volume only - $5M+ for liquidity self.pre_trade_validation = False # DISABLE validation - direct entry self.scalping_mode_enabled = True self.scalping_aggression = 1.0 # Max aggression self.scalping_target_rr = 1.5 self.scalping_profit_target = 0.225 # 0.225% target (HALF of normal 0.45%) self.scalping_stop_loss = 0.15 # 0.15% stop (1:1.5 ratio, HALF of normal 0.3%) self.order_type = "MARKET" # Fastest entry self.regime_filter_enabled = False # Trade all regimes self.quote_currency = "USDC" # Force USDC only for liquidity # ENABLE Falling Knife Monitor for Money Maker (catch bottoms) self.falling_knife_monitor_enabled = True self.falling_knife_aggression = 1.0 # Max aggression self.log_activity("[b][MONEY BULLDOZER][/b] Falling Knife Monitor ENABLED", "WARN") # DISABLE restrictive modes that block setups big_brain_was_on = getattr(self, 'big_brain_enabled', False) optimus_was_on = getattr(self, 'optimus_override', False) if big_brain_was_on: self.big_brain_enabled = False self.log_activity("[b][MONEY BULLDOZER][/b] Big Brain AUTO-DISABLED", "WARN") if optimus_was_on: self.optimus_override = False self.log_activity("[b][MONEY BULLDOZER][/b] Optimus Brain AUTO-DISABLED", "WARN") # Store what we disabled so we can restore it later self._mm_disabled_big_brain = big_brain_was_on self._mm_disabled_optimus = optimus_was_on # AGGRESSIVE: Clear all validation timeouts to unlock pairs timeout_count = len(self.validation_timeouts) if timeout_count > 0: self.validation_timeouts.clear() self.log_activity(f"[b][MONEY BULLDOZER][/b] Cleared {timeout_count} timeouts", "WARN") # AGGRESSIVE: Clear blocked pairs list blocked_count = len(self.blocked_pairs) if hasattr(self, 'blocked_pairs') else 0 if blocked_count > 0: self.blocked_pairs.clear() self.log_activity(f"[b][MONEY BULLDOZER][/b] Cleared {blocked_count} blocked pairs", "WARN") # AGGRESSIVE: Set minimum grade to F (allow all setups) self._mm_original_grade = getattr(self, 'min_trade_grade', 'F') self.min_trade_grade = "F" self.min_grade_filter = "F" self.log_activity("[b][MONEY BULLDOZER][/b] Grade filter set to F", "WARN") # AGGRESSIVE: Disable ALL time restrictions self._mm_original_time_restrict = getattr(self, 'time_restrictions_enabled', True) self.time_restrictions_enabled = False self.log_activity("[b][MONEY BULLDOZER][/b] Time restrictions DISABLED", "WARN") # AGGRESSIVE: Set max simultaneous trades higher self._mm_original_max_trades = getattr(self, 'max_simultaneous', 3) self.max_simultaneous = 10 # Allow more concurrent trades self.log_activity("[b][MONEY BULLDOZER][/b] Max trades: 10 | Volume: $5M+", "WARN") # Schedule session monitor Clock.schedule_interval(self._money_maker_heartbeat, 30) # Check every 30 seconds self.log_activity("[b]💰 MONEY MAKER MODE ACTIVATED 💰[/b]", "WARN") self.log_activity(f"Target: ${self.money_maker_target_profit} | Max Loss: ${self.money_maker_max_loss}", "WARN") self.log_activity(f"R/R: 1:{self.money_maker_rr_ratio} | Leverage: {self.money_maker_base_leverage}x", "WARN") self.log_activity(f"Session ID: {getattr(self, '_mm_session_id', 'N/A')}", "INFO") # Update UI self._update_mm_card_ui() # Force immediate scan self.force_market_scan() def _stop_money_maker_session(self, reason="Manual"): """Stop Money Maker session and report results.""" import time self.money_maker_mode = False session_duration = 0 if self.money_maker_session_start: session_duration = time.time() - self.money_maker_session_start hours = session_duration / 3600 else: hours = 0 # Restore original settings self._mm_restore_settings() # Restore Big Brain and Optimus if they were auto-disabled if getattr(self, '_mm_disabled_big_brain', False): self.big_brain_enabled = True self.log_activity("[b][MONEY BULLDOZER][/b] Big Brain restored", "INFO") if getattr(self, '_mm_disabled_optimus', False): self.optimus_override = True self.log_activity("[b][MONEY BULLDOZER][/b] Optimus Brain restored", "INFO") # Restore grade filter if hasattr(self, '_mm_original_grade'): self.min_trade_grade = self._mm_original_grade self.min_grade_filter = self._mm_original_grade self.log_activity(f"[b][MONEY BULLDOZER][/b] Grade restored to {self._mm_original_grade}", "INFO") # Restore time restrictions if hasattr(self, '_mm_original_time_restrict'): self.time_restrictions_enabled = self._mm_original_time_restrict self.log_activity("[b][MONEY BULLDOZER][/b] Time restrictions restored", "INFO") # Restore max trades if hasattr(self, '_mm_original_max_trades'): self.max_simultaneous = self._mm_original_max_trades self.log_activity(f"[b][MONEY BULLDOZER][/b] Max trades restored to {self._mm_original_max_trades}", "INFO") # Restore pre-trade validation self.pre_trade_validation = True self.log_activity("[b][MONEY BULLDOZER][/b] Pre-trade validation restored", "INFO") # Restore Falling Knife settings self.falling_knife_monitor_enabled = getattr(self, '_mm_original_fk_enabled', False) self.falling_knife_aggression = getattr(self, '_mm_original_fk_aggression', 0.5) self.log_activity("[b][MONEY BULLDOZER][/b] Falling Knife settings restored", "INFO") # Clear the disabled flags self._mm_disabled_big_brain = False self._mm_disabled_optimus = False # Unschedule heartbeat Clock.unschedule(self._money_maker_heartbeat) # Report profit = self.money_maker_session_profit status = "✅ TARGET HIT!" if profit >= self.money_maker_target_profit else "⏹️ STOPPED" self.log_activity(f"[b]💰 MONEY BULLDOZER {status}[/b]", "SUCCESS" if profit > 0 else "WARN") self.log_activity(f"Session P/L: ${profit:.2f} | Trades: {self.money_maker_trade_count} | Time: {hours:.1f}h", "INFO") # Log session summary to file self._log_mm_session_summary(profit, self.money_maker_trade_count, session_duration, reason) # Update UI self._update_mm_card_ui() return profit def _mm_save_original_settings(self): """Save current settings before MM override.""" self._mm_original = { 'min_rr_ratio': getattr(self, 'min_rr_ratio', 2.0), 'volume_threshold': getattr(self, 'volume_threshold', 500000), 'order_type': getattr(self, 'order_type', 'MARKET'), 'regime_filter_enabled': getattr(self, 'regime_filter_enabled', True), 'scalping_mode_enabled': getattr(self, 'scalping_mode_enabled', True), 'scalping_aggression': getattr(self, 'scalping_aggression', 0.7), 'quote_currency': getattr(self, 'quote_currency', 'USDC'), 'falling_knife_monitor_enabled': getattr(self, 'falling_knife_monitor_enabled', False), 'falling_knife_aggression': getattr(self, 'falling_knife_aggression', 0.5), 'time_restrictions_enabled': getattr(self, 'time_restrictions_enabled', True), 'max_simultaneous': getattr(self, 'max_simultaneous', 3), } def _mm_restore_settings(self): """Restore original settings after MM session.""" if hasattr(self, '_mm_original'): for key, value in self._mm_original.items(): setattr(self, key, value) def _money_maker_heartbeat(self, dt): """Monitor Money Maker session progress.""" import time if not self.money_maker_mode: return False # Check if target hit if self.money_maker_session_profit >= self.money_maker_target_profit: self.log_activity(f"[b]🎯 TARGET ACHIEVED! ${self.money_maker_session_profit:.2f} profit![/b]", "SUCCESS") self._stop_money_maker_session("Target Hit") return False # Check max loss if self.money_maker_session_profit <= -self.money_maker_max_loss: self.log_activity(f"[b]🛑 MAX LOSS REACHED! ${self.money_maker_session_profit:.2f}[/b]", "ERROR") self._stop_money_maker_session("Max Loss") return False # Check session timeout (6 hours) if self.money_maker_session_start: elapsed = time.time() - self.money_maker_session_start if elapsed >= self.money_maker_session_duration: self.log_activity(f"[b]⏰ SESSION TIMEOUT! ${self.money_maker_session_profit:.2f} P/L[/b]", "WARN") self._stop_money_maker_session("Timeout") return False # Log progress every 5 minutes if int(elapsed) % 300 < 30: remaining = self.money_maker_target_profit - self.money_maker_session_profit self.log_activity(f"💰 MM Progress: ${self.money_maker_session_profit:.2f} | Need ${remaining:.2f} more", "INFO") return True def _calculate_money_maker_position_size(self, entry_price, stop_price, direction): """Calculate position size for Money Maker trade. Formula: Risk $30 per trade (2% of $1500), 5x leverage, 0.3% stop Position = Risk Amount / (Stop % * Entry Price) """ risk_amount = 30.0 # $30 risk per trade (2% of available capital) stop_pct = abs(entry_price - stop_price) / entry_price if stop_pct == 0: stop_pct = 0.003 # Default 0.3% # Position size in base currency position_value = risk_amount / stop_pct # Apply leverage leveraged_position = position_value * self.money_maker_base_leverage # Cap at available capital max_position = self.money_maker_compound_capital * 0.8 # Use 80% max per trade final_position = min(leveraged_position, max_position) return final_position def _on_money_maker_trade_close(self, trade_pnl): """Handle closed trade in Money Maker mode.""" self.money_maker_session_profit += trade_pnl self.money_maker_trade_count += 1 # Compounding: Add profit back to available capital if trade_pnl > 0: self.money_maker_compound_capital += trade_pnl * 0.5 # Reinvest 50% of profit # Log progress progress_pct = (self.money_maker_session_profit / self.money_maker_target_profit) * 100 self.log_activity(f"💰 Trade #{self.money_maker_trade_count}: ${trade_pnl:+.2f} | Session: ${self.money_maker_session_profit:.2f} ({progress_pct:.0f}%)", "SUCCESS" if trade_pnl > 0 else "ERROR") # Check if we should continue self._money_maker_heartbeat(None) def _money_maker_scan_strategy(self, pair, ohlcv_1m, ohlcv_5m): """Money Maker specific entry strategy. Uses 3 uncorrelated strategies: 1. VWAP Reversion - Price extends >0.5% from VWAP, revert 2. Momentum Burst - 3 consecutive candles in direction 3. Breakout Scalp - Break of 15-min range with volume """ if not self.money_maker_mode: return None import random # Check if we've hit trade limit if self.money_maker_trade_count >= self.money_maker_max_trades: return None # Check for existing position in this pair if hasattr(self, 'active_trades'): for trade in self.active_trades: if trade.get('pair') == pair: return None # Already in this pair signals = [] # Strategy 1: VWAP Reversion vwap_signal = self._mm_vwap_strategy(pair, ohlcv_1m) if vwap_signal: signals.append(('VWAP', vwap_signal)) # Strategy 2: Momentum Burst mom_signal = self._mm_momentum_strategy(pair, ohlcv_1m) if mom_signal: signals.append(('MOMENTUM', mom_signal)) # Strategy 3: Breakout Scalp break_signal = self._mm_breakout_strategy(pair, ohlcv_5m) if break_signal: signals.append(('BREAKOUT', break_signal)) # Take the strongest signal if signals: # Sort by confidence if available, else random signals.sort(key=lambda x: x[1].get('confidence', 0), reverse=True) best_strategy, best_signal = signals[0] best_signal['strategy'] = best_strategy best_signal['money_maker'] = True return best_signal return None def _mm_vwap_strategy(self, pair, ohlcv): """VWAP mean reversion strategy.""" if not ohlcv or len(ohlcv) < 20: return None import numpy as np closes = np.array([c[4] for c in ohlcv[-20:]]) volumes = np.array([c[5] for c in ohlcv[-20:]]) # Calculate VWAP typical_prices = np.array([(c[1] + c[2] + c[4]) / 3 for c in ohlcv[-20:]]) vwap = np.sum(typical_prices * volumes) / np.sum(volumes) current_price = closes[-1] deviation = (current_price - vwap) / vwap * 100 # Price extended above VWAP - look for short if deviation > 0.5: return { 'direction': 'SHORT', 'entry': current_price, 'stop': current_price * 1.0015, # 0.15% above (HALF) 'target': current_price * 0.99775, # 0.225% below (HALF) 'confidence': min(abs(deviation) * 20, 90), 'reason': f'VWAP reversion: {deviation:.2f}% above' } # Price extended below VWAP - look for long if deviation < -0.5: return { 'direction': 'LONG', 'entry': current_price, 'stop': current_price * 0.9985, # 0.15% below (HALF) 'target': current_price * 1.00225, # 0.225% above (HALF) 'confidence': min(abs(deviation) * 20, 90), 'reason': f'VWAP reversion: {deviation:.2f}% below' } return None def _mm_momentum_strategy(self, pair, ohlcv): """Momentum burst strategy.""" if not ohlcv or len(ohlcv) < 5: return None closes = [c[4] for c in ohlcv[-5:]] # 3 consecutive higher closes = bullish momentum if closes[-1] > closes[-2] > closes[-3] > closes[-4]: return { 'direction': 'LONG', 'entry': closes[-1], 'stop': closes[-1] * 0.9985, # 0.15% below (HALF) 'target': closes[-1] * 1.00225, # 0.225% above (HALF) 'confidence': 75, 'reason': '3-candle bullish momentum' } # 3 consecutive lower closes = bearish momentum if closes[-1] < closes[-2] < closes[-3] < closes[-4]: return { 'direction': 'SHORT', 'entry': closes[-1], 'stop': closes[-1] * 1.0015, # 0.15% above (HALF) 'target': closes[-1] * 0.99775, # 0.225% below (HALF) 'confidence': 75, 'reason': '3-candle bearish momentum' } return None def _mm_breakout_strategy(self, pair, ohlcv): """Breakout scalp strategy using 15-min range.""" if not ohlcv or len(ohlcv) < 4: return None # Get 15-min range (3 candles of 5m) highs = [c[2] for c in ohlcv[-4:-1]] lows = [c[3] for c in ohlcv[-4:-1]] range_high = max(highs) range_low = min(lows) current_price = ohlcv[-1][4] current_volume = ohlcv[-1][5] avg_volume = sum([c[5] for c in ohlcv[-10:-1]]) / 9 # Breakout above range with volume if current_price > range_high * 1.001 and current_volume > avg_volume * 1.2: return { 'direction': 'LONG', 'entry': current_price, 'stop': range_low, 'target': current_price + (current_price - range_low) * 0.75, # HALF range 'confidence': 80, 'reason': f'Breakout above {range_high:.4f}' } # Breakdown below range with volume if current_price < range_low * 0.999 and current_volume > avg_volume * 1.2: return { 'direction': 'SHORT', 'entry': current_price, 'stop': range_high, 'target': current_price - (range_high - current_price) * 0.75, # HALF range 'confidence': 80, 'reason': f'Breakdown below {range_low:.4f}' } return None # ============================================ # MONEY MAKER PERFORMANCE TRACKER METHODS # ============================================ def _ensure_mm_log_dir(self): """Create money_maker_logs directory if it doesn't exist.""" import os if not os.path.exists(self.mm_log_dir): try: os.makedirs(self.mm_log_dir) print(f"[MM TRACKER] Created log directory: {self.mm_log_dir}") except Exception as e: print(f"[MM TRACKER] Error creating log dir: {e}") def _load_mm_tracker(self): """Load historical tracker data from file.""" import os import json tracker_file = os.path.join(self.mm_log_dir, "mm_tracker_stats.json") if os.path.exists(tracker_file): try: with open(tracker_file, 'r') as f: loaded = json.load(f) self.mm_tracker.update(loaded) print(f"[MM TRACKER] Loaded historical stats: {self.mm_tracker['total_sessions']} sessions") except Exception as e: print(f"[MM TRACKER] Error loading stats: {e}") def _save_mm_tracker(self): """Save tracker data to file.""" import json import os tracker_file = os.path.join(self.mm_log_dir, "mm_tracker_stats.json") try: with open(tracker_file, 'w') as f: json.dump(self.mm_tracker, f, indent=2) except Exception as e: print(f"[MM TRACKER] Error saving stats: {e}") def _log_mm_trade_entry(self, pair, direction, entry_price, stop_price, target_price, strategy, position_size): """Log trade entry to file.""" import json import time from datetime import datetime log_entry = { 'timestamp': datetime.now().isoformat(), 'unix_time': time.time(), 'type': 'ENTRY', 'session_id': getattr(self, '_mm_session_id', 'unknown'), 'pair': pair, 'direction': direction, 'entry_price': entry_price, 'stop_price': stop_price, 'target_price': target_price, 'strategy': strategy, 'position_size': position_size, 'leverage': self.money_maker_base_leverage } self._write_mm_log(log_entry) def _log_mm_trade_exit(self, pair, entry_price, exit_price, direction, pnl, strategy, exit_reason, hold_time_seconds): """Log trade exit to file and update tracker.""" import json import time from datetime import datetime log_entry = { 'timestamp': datetime.now().isoformat(), 'unix_time': time.time(), 'type': 'EXIT', 'session_id': getattr(self, '_mm_session_id', 'unknown'), 'pair': pair, 'direction': direction, 'entry_price': entry_price, 'exit_price': exit_price, 'pnl': pnl, 'strategy': strategy, 'exit_reason': exit_reason, 'hold_time_seconds': hold_time_seconds } self._write_mm_log(log_entry) # Update tracker stats self.mm_tracker['total_trades'] += 1 if pnl > 0: self.mm_tracker['winning_trades'] += 1 self.mm_tracker['total_profit'] += pnl else: self.mm_tracker['losing_trades'] += 1 self.mm_tracker['total_loss'] += abs(pnl) # Update strategy performance if strategy in self.mm_tracker['strategy_performance']: self.mm_tracker['strategy_performance'][strategy]['trades'] += 1 if pnl > 0: self.mm_tracker['strategy_performance'][strategy]['wins'] += 1 self.mm_tracker['strategy_performance'][strategy]['profit'] += pnl # Recalculate derived stats self._recalculate_mm_stats() self._save_mm_tracker() def _log_mm_session_summary(self, session_pnl, session_trades, session_duration, stop_reason): """Log session summary when Money Maker ends.""" import json import time from datetime import datetime log_entry = { 'timestamp': datetime.now().isoformat(), 'unix_time': time.time(), 'type': 'SESSION_SUMMARY', 'session_id': getattr(self, '_mm_session_id', 'unknown'), 'session_pnl': session_pnl, 'total_trades': session_trades, 'duration_seconds': session_duration, 'stop_reason': stop_reason, 'target': self.money_maker_target_profit, 'max_loss': self.money_maker_max_loss } self._write_mm_log(log_entry) # Update tracker stats self.mm_tracker['total_sessions'] += 1 if session_pnl >= self.money_maker_target_profit: self.mm_tracker['profitable_sessions'] += 1 elif session_pnl < 0: self.mm_tracker['losing_sessions'] += 1 # Track best/worst sessions if session_pnl > self.mm_tracker['best_session']: self.mm_tracker['best_session'] = session_pnl if session_pnl < self.mm_tracker['worst_session']: self.mm_tracker['worst_session'] = session_pnl self._recalculate_mm_stats() self._save_mm_tracker() def _write_mm_log(self, entry): """Write entry to daily log file.""" import json import os from datetime import datetime date_str = datetime.now().strftime('%Y-%m-%d') log_file = os.path.join(self.mm_log_dir, f"mm_trades_{date_str}.jsonl") try: with open(log_file, 'a') as f: f.write(json.dumps(entry) + '\n') except Exception as e: print(f"[MM TRACKER] Error writing log: {e}") def _recalculate_mm_stats(self): """Recalculate derived statistics.""" total = self.mm_tracker['total_trades'] if total > 0: self.mm_tracker['win_rate'] = (self.mm_tracker['winning_trades'] / total) * 100 self.mm_tracker['avg_trade_pnl'] = (self.mm_tracker['total_profit'] - self.mm_tracker['total_loss']) / total sessions = self.mm_tracker['total_sessions'] if sessions > 0: total_pnl = self.mm_tracker['total_profit'] - self.mm_tracker['total_loss'] self.mm_tracker['avg_session_pnl'] = total_pnl / sessions def _generate_mm_session_id(self): """Generate unique session ID.""" import time import random from datetime import datetime self._mm_session_id = f"MM_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{random.randint(1000,9999)}" return self._mm_session_id def show_money_maker_performance_report(self): """Show comprehensive performance report popup.""" content = BoxLayout(orientation='vertical', spacing=dp(6), padding=dp(12)) # Title content.add_widget(Label( text="[b]💰 MONEY MAKER PERFORMANCE REPORT[/b]", markup=True, color=GOLD, font_size=sp(14), size_hint_y=None, height=dp(35) )) # Scrollable stats area scroll = ScrollView(size_hint=(1, 0.75)) stats_layout = BoxLayout(orientation='vertical', size_hint_y=None, spacing=dp(4)) stats_layout.bind(minimum_height=stats_layout.setter('height')) def add_stat_row(label, value, color=WHITE): row = BoxLayout(size_hint_y=None, height=dp(26)) row.add_widget(Label(text=label, color=GRAY, font_size=sp(11), halign='left', size_hint_x=0.6)) row.add_widget(Label(text=value, color=color, font_size=sp(11), halign='right', size_hint_x=0.4, markup=True)) stats_layout.add_widget(row) # Session Stats stats_layout.add_widget(Label(text="[b]SESSION STATS[/b]", markup=True, color=CYAN, font_size=sp(12), size_hint_y=None, height=dp(28))) add_stat_row("Total Sessions:", f"{self.mm_tracker['total_sessions']}") add_stat_row("Profitable Sessions:", f"{self.mm_tracker['profitable_sessions']}", GREEN) add_stat_row("Losing Sessions:", f"{self.mm_tracker['losing_sessions']}", RED) add_stat_row("Session Win Rate:", f"{(self.mm_tracker['profitable_sessions']/max(self.mm_tracker['total_sessions'],1)*100):.1f}%", GREEN if self.mm_tracker['profitable_sessions'] > self.mm_tracker['losing_sessions'] else RED) # Trade Stats stats_layout.add_widget(Label(text="[b]TRADE STATS[/b]", markup=True, color=CYAN, font_size=sp(12), size_hint_y=None, height=dp(28))) add_stat_row("Total Trades:", f"{self.mm_tracker['total_trades']}") add_stat_row("Winning Trades:", f"{self.mm_tracker['winning_trades']}", GREEN) add_stat_row("Losing Trades:", f"{self.mm_tracker['losing_trades']}", RED) add_stat_row("Trade Win Rate:", f"{self.mm_tracker['win_rate']:.1f}%", GREEN if self.mm_tracker['win_rate'] > 50 else AMBER if self.mm_tracker['win_rate'] > 40 else RED) # P&L Stats stats_layout.add_widget(Label(text="[b]P&L STATS[/b]", markup=True, color=CYAN, font_size=sp(12), size_hint_y=None, height=dp(28))) total_pnl = self.mm_tracker['total_profit'] - self.mm_tracker['total_loss'] add_stat_row("Total Profit:", f"+${self.mm_tracker['total_profit']:.2f}", GREEN) add_stat_row("Total Loss:", f"-${self.mm_tracker['total_loss']:.2f}", RED) add_stat_row("Net P&L:", f"${total_pnl:+.2f}", GREEN if total_pnl > 0 else RED) add_stat_row("Avg per Trade:", f"${self.mm_tracker['avg_trade_pnl']:.2f}", GREEN if self.mm_tracker['avg_trade_pnl'] > 0 else RED) add_stat_row("Avg per Session:", f"${self.mm_tracker['avg_session_pnl']:.2f}", GREEN if self.mm_tracker['avg_session_pnl'] > 0 else RED) add_stat_row("Best Session:", f"+${self.mm_tracker['best_session']:.2f}", GREEN) add_stat_row("Worst Session:", f"${self.mm_tracker['worst_session']:.2f}", RED) # Strategy Performance stats_layout.add_widget(Label(text="[b]STRATEGY PERFORMANCE[/b]", markup=True, color=CYAN, font_size=sp(12), size_hint_y=None, height=dp(28))) for strategy, stats in self.mm_tracker['strategy_performance'].items(): if stats['trades'] > 0: win_rate = (stats['wins'] / stats['trades']) * 100 add_stat_row(f"{strategy}:", f"{stats['trades']} trades | {win_rate:.0f}% WR | ${stats['profit']:+.2f}", GREEN if stats['profit'] > 0 else RED) scroll.add_widget(stats_layout) content.add_widget(scroll) # Buttons buttons = BoxLayout(size_hint_y=None, height=dp(45), spacing=dp(10)) close_btn = StyledButton( text="[b]CLOSE[/b]", markup=True, bg_color=GRAY, text_color=WHITE, font_size=sp(12), radius=8 ) close_btn.bind(on_press=lambda x: popup.dismiss()) buttons.add_widget(close_btn) export_btn = StyledButton( text="[b]📊 EXPORT LOGS[/b]", markup=True, bg_color=CYAN, text_color=BLACK, font_size=sp(12), radius=8 ) export_btn.bind(on_press=lambda x: (self._export_mm_logs(), popup.dismiss())) buttons.add_widget(export_btn) content.add_widget(buttons) popup = Popup( title='[b]💰 MM PERFORMANCE[/b]', content=content, size_hint=(0.9, 0.85), background_color=DARK_BG, title_color=GOLD ) popup.open() def _export_mm_logs(self): """Export Money Maker logs to a summary file.""" import os import json from datetime import datetime export_file = os.path.join(self.mm_log_dir, f"mm_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt") try: with open(export_file, 'w') as f: f.write("="*60 + "\n") f.write("MONEY MAKER PERFORMANCE EXPORT\n") f.write(f"Generated: {datetime.now().isoformat()}\n") f.write("="*60 + "\n\n") f.write("OVERALL STATISTICS:\n") f.write(f"Total Sessions: {self.mm_tracker['total_sessions']}\n") f.write(f"Profitable Sessions: {self.mm_tracker['profitable_sessions']}\n") f.write(f"Losing Sessions: {self.mm_tracker['losing_sessions']}\n") f.write(f"Session Win Rate: {(self.mm_tracker['profitable_sessions']/max(self.mm_tracker['total_sessions'],1)*100):.1f}%\n\n") f.write("TRADE STATISTICS:\n") f.write(f"Total Trades: {self.mm_tracker['total_trades']}\n") f.write(f"Winning Trades: {self.mm_tracker['winning_trades']}\n") f.write(f"Losing Trades: {self.mm_tracker['losing_trades']}\n") f.write(f"Trade Win Rate: {self.mm_tracker['win_rate']:.1f}%\n") f.write(f"Avg Trade P&L: ${self.mm_tracker['avg_trade_pnl']:.2f}\n\n") f.write("P&L BREAKDOWN:\n") total_pnl = self.mm_tracker['total_profit'] - self.mm_tracker['total_loss'] f.write(f"Total Profit: ${self.mm_tracker['total_profit']:.2f}\n") f.write(f"Total Loss: ${self.mm_tracker['total_loss']:.2f}\n") f.write(f"Net P&L: ${total_pnl:.2f}\n") f.write(f"Best Session: ${self.mm_tracker['best_session']:.2f}\n") f.write(f"Worst Session: ${self.mm_tracker['worst_session']:.2f}\n\n") f.write("STRATEGY BREAKDOWN:\n") for strategy, stats in self.mm_tracker['strategy_performance'].items(): if stats['trades'] > 0: win_rate = (stats['wins'] / stats['trades']) * 100 f.write(f"{strategy}: {stats['trades']} trades, {win_rate:.1f}% WR, ${stats['profit']:+.2f}\n") f.write("\n" + "="*60 + "\n") f.write("END OF REPORT\n") self.log_activity(f"[b]MM REPORT:[/b] Exported to {export_file}", "SUCCESS") except Exception as e: self.log_activity(f"[b]MM EXPORT ERROR:[/b] {e}", "ERROR") def show_money_maker_status(self): """Display Money Maker session status popup.""" if not self.money_maker_mode: return import time content = BoxLayout(orientation='vertical', spacing=dp(8), padding=dp(12)) elapsed = time.time() - self.money_maker_session_start if self.money_maker_session_start else 0 hours_left = max(0, (self.money_maker_session_duration - elapsed) / 3600) progress = (self.money_maker_session_profit / self.money_maker_target_profit) * 100 # Status color based on progress if progress >= 100: status_color = GREEN status_text = "TARGET HIT! 🎯" elif progress > 50: status_color = GOLD status_text = "On Track ✓" elif progress > 0: status_color = AMBER status_text = "Building..." else: status_color = RED status_text = "Struggling" content.add_widget(Label( text=f"[b]💰 MONEY MAKER STATUS[/b]", markup=True, color=GOLD, font_size=sp(16), size_hint_y=None, height=dp(40) )) content.add_widget(Label( text=f"[b]{status_text}[/b]", markup=True, color=status_color, font_size=sp(20), size_hint_y=None, height=dp(40) )) content.add_widget(Label( text=f"P/L: ${self.money_maker_session_profit:.2f} / ${self.money_maker_target_profit:.0f} ({progress:.0f}%)", markup=True, color=WHITE, font_size=sp(14), size_hint_y=None, height=dp(30) )) content.add_widget(Label( text=f"Trades: {self.money_maker_trade_count} | Time Left: {hours_left:.1f}h", markup=True, color=GRAY, font_size=sp(12), size_hint_y=None, height=dp(25) )) content.add_widget(Label( text=f"Capital: ${self.money_maker_compound_capital:.0f} | Leverage: {self.money_maker_base_leverage}x", markup=True, color=CYAN, font_size=sp(11), size_hint_y=None, height=dp(25) )) # Progress bar visualization progress_box = BoxLayout(size_hint_y=None, height=dp(20), padding=dp(2)) progress_box.add_widget(Label( text="[" + "█" * int(progress / 5) + "░" * (20 - int(progress / 5)) + "]", markup=True, color=GOLD if progress > 0 else RED, font_size=sp(12) )) content.add_widget(progress_box) # Buttons row buttons = BoxLayout(size_hint_y=None, height=dp(45), spacing=dp(10), padding=dp(4)) report_btn = StyledButton( text="[b]📊 REPORT[/b]", markup=True, bg_color=CYAN, text_color=BLACK, font_size=sp(11), radius=8 ) report_btn.bind(on_press=lambda x: (self.show_money_maker_performance_report(), popup.dismiss())) buttons.add_widget(report_btn) close_btn = StyledButton( text="[b]CLOSE[/b]", markup=True, bg_color=GRAY, text_color=WHITE, font_size=sp(11), radius=8 ) close_btn.bind(on_press=lambda x: popup.dismiss()) buttons.add_widget(close_btn) content.add_widget(buttons) popup = Popup( title='[b]💰 MONEY MAKER[/b]', content=content, size_hint=(0.85, 0.55), background_color=DARK_BG, title_color=GOLD ) popup.open() def toggle_circuit_breaker(self, instance=None): """Stub for circuit breaker - feature removed in lite version""" self.log_activity("Circuit breaker: Feature disabled in lite version") if instance and hasattr(instance, 'text'): instance.text = "[b]OFF[/b]" if instance and hasattr(instance, 'bg_color'): instance.bg_color = GRAY def update_circuit_breaker_ui(self): """Stub for circuit breaker UI update - feature removed in lite version""" pass def update_dxy_ui(self): """Stub for DXY UI update - feature removed in lite version""" pass def _monitor_setup_queue_bg(self): """Stub for setup queue monitor - feature removed in lite version""" pass def update_sentinel_display(self): """Stub for SENTINEL display - feature removed in lite version""" pass def update_sentinel_monitor(self): """Stub for SENTINEL monitor - feature removed in lite version""" pass def update_thor_display(self): """Stub for THOR display - feature removed in lite version""" pass def update_funding_display(self): """Stub for funding display - feature removed in lite version""" pass def fetch_funding_rates(self): """Stub for funding rates - feature removed in lite version""" pass def toggle_dxy_filter(self, instance=None): """Toggle DXY filter.""" with self._filter_lock: self.dxy_filter_enabled = not self.dxy_filter_enabled dxy_now_enabled = self.dxy_filter_enabled # Only update button UI if called from a button (not MODES menu) if instance and hasattr(instance, 'text') and hasattr(instance, 'bg_color'): instance.text = "[b]ON[/b]" if dxy_now_enabled else "[b]OFF[/b]" instance.bg_color = GREEN if dxy_now_enabled else GRAY self.log_activity(f"DXY macro filter: {'ENABLED (adjust sizes based on dollar strength)' if dxy_now_enabled else 'DISABLED (ignore dollar index)'}") def toggle_all_limits(self, instance=None): """Toggle all limits off/on.""" with self._filter_lock: self.all_limits_off = not self.all_limits_off limits_now_off = self.all_limits_off if limits_now_off: self.min_grade_filter = "F" self.min_trade_grade = "F" else: self.min_grade_filter = "F" self.min_trade_grade = "F" # Only update button UI if called from a button (not MODES menu) if instance and hasattr(instance, 'text') and hasattr(instance, 'bg_color'): if limits_now_off: instance.text = "[b]ON[/b]" instance.bg_color = RED else: instance.text = "[b]OFF[/b]" instance.bg_color = DARK_RED if limits_now_off: self.log_activity("[b]WARNING: ALL LIMITS DISABLED![/b] Trading without any restrictions!", "ERROR") self.log_activity("Circuit breaker, time limits, grade filters, R/R filters ALL OFF", "WARN") else: self.log_activity("All limits restored - normal risk management active") def toggle_silent_mode(self, instance=None): """Toggle silent mode.""" self.silent_mode = not self.silent_mode # Only update button UI if called from a button (not MODES menu) if instance and hasattr(instance, 'text') and hasattr(instance, 'bg_color'): instance.text = "[b]ON[/b]" if self.silent_mode else "[b]OFF[/b]" instance.bg_color = GREEN if self.silent_mode else GRAY if self.silent_mode: self.log_activity("Silent mode: ON - Setup tracking logs disabled") else: self.log_activity("Silent mode: OFF - Full logging restored") def toggle_pre_trade_validation(self, instance=None): """Toggle pre-trade validation.""" with self._filter_lock: self.pre_trade_validation = not self.pre_trade_validation validation_enabled = self.pre_trade_validation # Only update button UI if called from a button (not MODES menu) if instance and hasattr(instance, 'text') and hasattr(instance, 'bg_color'): instance.text = "[b]ON[/b]" if validation_enabled else "[b]OFF[/b]" instance.bg_color = GREEN if validation_enabled else GRAY if validation_enabled: self.log_activity("Pre-trade validation: ON - Strict re-check before entry (safer)") else: self.log_activity("Pre-trade validation: OFF - Trading on original setup data (faster)") # Save settings self.save_all_settings() def toggle_lower_tf_priority(self, instance=None): """Toggle lower timeframe priority for multi-timeframe scans.""" self.lower_tf_priority = not getattr(self, 'lower_tf_priority', False) # Update button UI if instance and hasattr(instance, 'text') and hasattr(instance, 'bg_color'): instance.text = "[b]ON[/b]" if self.lower_tf_priority else "[b]OFF[/b]" instance.bg_color = GREEN if self.lower_tf_priority else DARK_GRAY if self.lower_tf_priority: self.log_activity("[b]LOWER TF PRIORITY:[/b] ON - M3/M5 weighted higher than H4/H2 (catches early reversals)") else: self.log_activity("[b]LOWER TF PRIORITY:[/b] OFF - H4/H2 weighted higher (trend following)") # Save settings self.save_all_settings() def toggle_consensus_threshold(self, instance=None): """Toggle minimum consensus threshold (4 or 5 of 6 timeframes).""" current = getattr(self, 'min_consensus', 4) # Toggle between 4 and 5 self.min_consensus = 5 if current == 4 else 4 # Update button UI if instance and hasattr(instance, 'text') and hasattr(instance, 'bg_color'): instance.text = f"[b]{self.min_consensus} of 6[/b]" instance.bg_color = GREEN if self.min_consensus >= 5 else CYAN self.log_activity(f"[b]CONSENSUS:[/b] Require {self.min_consensus} of 6 timeframes to agree ({'strict' if self.min_consensus >= 5 else 'balanced'})") # Save settings self.save_all_settings() def clear_all_timeouts(self, instance=None): """Clear all validation timeouts - useful when bot gets stuck""" timeout_count = len(self.validation_timeouts) if timeout_count > 0: self.validation_timeouts.clear() self.log_activity(f"[b]TIMEOUTS CLEARED:[/b] {timeout_count} pair(s) removed from timeout", "SUCCESS") # Also trigger a fresh scan self._scan_completed = False self.log_activity("[b]REFRESH:[/b] Triggering fresh market scan...", "INFO") Clock.schedule_once(lambda dt: self.force_market_scan(), 0.5) else: self.log_activity("[b]TIMEOUTS:[/b] No active timeouts to clear", "INFO") def show_backtest_report(self, instance=None): report_file = self.backtest_logger.export_analysis_report() summary = self.backtest_logger.get_validation_summary() content = BoxLayout(orientation='vertical', spacing=dp(10), padding=dp(15)) summary_lbl = Label( text=summary, markup=True, color=WHITE, font_size=sp(11), size_hint_y=0.6, halign='left', valign='top' ) summary_lbl.bind(size=lambda lbl, sz: setattr(lbl, 'text_size', sz)) content.add_widget(summary_lbl) btn_row = BoxLayout(size_hint_y=0.15, spacing=dp(10)) export_btn = StyledButton( text="[b]EXPORT CSV[/b]", markup=True, bg_color=BLUE, text_color=WHITE, font_size=sp(11), radius=8 ) reset_btn = StyledButton( text="[b]NEW SESSION[/b]", markup=True, bg_color=AMBER, text_color=WHITE, font_size=sp(11), radius=8 ) close_btn = StyledButton( text="[b]CLOSE[/b]", markup=True, bg_color=GRAY, text_color=WHITE, font_size=sp(11), radius=8 ) btn_row.add_widget(export_btn) btn_row.add_widget(reset_btn) btn_row.add_widget(close_btn) content.add_widget(btn_row) info_lbl = Label( text=f"Session: {self.backtest_logger.session_id}\nFiles saved to: data/backtests/", color=GRAY, font_size=sp(9), size_hint_y=0.1 ) content.add_widget(info_lbl) popup = Popup( title="Backtest Analysis Report", content=content, size_hint=(0.9, 0.7), background_color=CARD_BG ) def export_csv(btn): self.backtest_logger.save_session() self.log_activity(f"[b]BACKTEST:[/b] Data exported to data/backtests/", "INFO") def reset_session(btn): self.backtest_logger.reset_session() self.log_activity("[b]BACKTEST:[/b] New session started", "INFO") popup.dismiss() export_btn.bind(on_press=export_csv) reset_btn.bind(on_press=reset_session) close_btn.bind(on_press=popup.dismiss) popup.open() def toggle_time_restrictions(self, instance=None): """Toggle time restrictions.""" self.time_restrictions_enabled = not self.time_restrictions_enabled # Only update button UI if called from a button (not MODES menu) if instance and hasattr(instance, 'text') and hasattr(instance, 'bg_color'): instance.text = "[b]ON[/b]" if self.time_restrictions_enabled else "[b]OFF[/b]" instance.bg_color = GREEN if self.time_restrictions_enabled else GRAY if self.time_restrictions_enabled: self.log_activity("Time restrictions: ENABLED") else: self.log_activity("Time restrictions: DISABLED - Bot can trade anytime!") self.update_protocol_info() def toggle_trading_mode(self, instance=None): """Toggle trading mode between 24/7 and custom hours.""" if self.trading_hours_mode == "24/7": self.trading_hours_mode = "custom" if instance and hasattr(instance, 'text') and hasattr(instance, 'bg_color'): instance.text = "[b]CUSTOM[/b]" instance.bg_color = AMBER self.log_activity("Trading hours: CUSTOM - Configure your trading window") else: self.trading_hours_mode = "24/7" if instance and hasattr(instance, 'text') and hasattr(instance, 'bg_color'): instance.text = "[b]24/7[/b]" instance.bg_color = GREEN self.log_activity("Trading hours: 24/7 - Always trading") self.update_protocol_info() def show_trading_hours_config(self, instance): from kivy.uix.popup import Popup from kivy.uix.gridlayout import GridLayout content = BoxLayout(orientation='vertical', padding=dp(10), spacing=dp(8)) content.add_widget(Label(text="[b]Configure Trading Hours[/b]", markup=True, color=GOLD, font_size=sp(14))) content.add_widget(Label(text="[b]Active Weekdays:[/b]", markup=True, color=CYAN, font_size=sp(11))) days_grid = GridLayout(cols=7, spacing=dp(4), padding=dp(4), size_hint_y=None, height=dp(50)) day_names = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] self.day_buttons = [] for i, name in enumerate(day_names): is_active = i in self.trading_weekdays btn = StyledButton( text=f"[b]{name}[/b]", markup=True, bg_color=GREEN if is_active else GRAY, text_color=WHITE, font_size=sp(9), radius=6 ) btn.day_index = i btn.bind(on_press=lambda x: self.toggle_weekday(x)) days_grid.add_widget(btn) self.day_buttons.append(btn) content.add_widget(days_grid) weekend_row = BoxLayout(size_hint_y=None, height=dp(40), spacing=dp(8)) weekend_row.add_widget(Label(text="Weekend Trading:", color=WHITE, font_size=sp(11), size_hint_x=0.5)) self.weekend_btn = StyledButton( text="[b]ON[/b]" if self.trading_weekend_enabled else "[b]OFF[/b]", markup=True, bg_color=GREEN if self.trading_weekend_enabled else GRAY, text_color=WHITE, font_size=sp(11), radius=8, size_hint_x=0.5 ) self.weekend_btn.bind(on_press=self.toggle_weekend) weekend_row.add_widget(self.weekend_btn) content.add_widget(weekend_row) content.add_widget(Label(text="[b]Trading Window:[/b]", markup=True, color=CYAN, font_size=sp(11))) time_grid = GridLayout(cols=4, spacing=dp(6), padding=dp(4), size_hint_y=None, height=dp(50)) start_h, start_m = self.trading_start_time stop_h, stop_m = self.trading_stop_time time_grid.add_widget(Label(text="Start:", color=GRAY, font_size=sp(10))) self.start_time_btn = StyledButton( text=f"[b]{start_h:02d}:{start_m:02d}[/b]", markup=True, bg_color=BLUE, text_color=WHITE, font_size=sp(11), radius=6 ) self.start_time_btn.bind(on_press=lambda x: self.show_time_picker('start')) time_grid.add_widget(self.start_time_btn) time_grid.add_widget(Label(text="Stop:", color=GRAY, font_size=sp(10))) self.stop_time_btn = StyledButton( text=f"[b]{stop_h:02d}:{stop_m:02d}[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(11), radius=6 ) self.stop_time_btn.bind(on_press=lambda x: self.show_time_picker('stop')) time_grid.add_widget(self.stop_time_btn) content.add_widget(time_grid) content.add_widget(Label( text=f"When window closes: Positions trail at {self.trailing_stop_pct}%", color=GRAY, font_size=sp(9), size_hint_y=None, height=dp(30) )) close_btn = StyledButton( text="[b]DONE[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(12), radius=10, size_hint_y=None, height=dp(45) ) content.add_widget(close_btn) self.trading_hours_popup = Popup( title="Trading Hours", content=content, size_hint=(0.85, 0.6), background_color=CARD_BG ) close_btn.bind(on_press=self.trading_hours_popup.dismiss) self.trading_hours_popup.open() def toggle_weekday(self, btn): day = btn.day_index if day in self.trading_weekdays: self.trading_weekdays.remove(day) btn.bg_color = GRAY else: self.trading_weekdays.append(day) btn.bg_color = GREEN self.trading_weekdays.sort() self.update_protocol_info() def toggle_weekend(self, instance): self.trading_weekend_enabled = not self.trading_weekend_enabled instance.text = "[b]ON[/b]" if self.trading_weekend_enabled else "[b]OFF[/b]" instance.bg_color = GREEN if self.trading_weekend_enabled else GRAY self.update_protocol_info() def toggle_weekend_quick(self, instance): self.trading_weekend_enabled = not self.trading_weekend_enabled if self.trading_weekend_enabled: instance.text = "[b]WEEKEND: ON[/b]" instance.bg_color = GREEN self.log_activity("Weekend trading: ENABLED") else: instance.text = "[b]WEEKEND: OFF[/b]" instance.bg_color = GRAY self.log_activity("Weekend trading: DISABLED") self.update_protocol_info() def show_time_picker(self, which): from kivy.uix.popup import Popup from kivy.uix.gridlayout import GridLayout is_start = which == 'start' current = self.trading_start_time if is_start else self.trading_stop_time content = BoxLayout(orientation='vertical', padding=dp(10), spacing=dp(8)) content.add_widget(Label(text=f"[b]Set {'Start' if is_start else 'Stop'} Time[/b]", markup=True, color=GOLD, font_size=sp(14))) content.add_widget(Label(text="Hour:", color=CYAN, font_size=sp(11))) hour_grid = GridLayout(cols=6, spacing=dp(4), padding=dp(4), size_hint_y=None, height=dp(160)) for h in range(24): btn = StyledButton( text=f"[b]{h:02d}[/b]", markup=True, bg_color=BLUE if h == current[0] else CARD_BG, text_color=WHITE, font_size=sp(11), radius=6 ) btn.hour = h btn.bind(on_press=lambda x: self.set_time_hour(which, x.hour)) hour_grid.add_widget(btn) content.add_widget(hour_grid) content.add_widget(Label(text="Minute:", color=CYAN, font_size=sp(11))) min_grid = GridLayout(cols=6, spacing=dp(4), padding=dp(4), size_hint_y=None, height=dp(60)) for m in [0, 15, 30, 45]: btn = StyledButton( text=f"[b]{m:02d}[/b]", markup=True, bg_color=BLUE if m == current[1] else CARD_BG, text_color=WHITE, font_size=sp(11), radius=6 ) btn.minute = m btn.bind(on_press=lambda x: self.set_time_minute(which, x.minute)) min_grid.add_widget(btn) content.add_widget(min_grid) popup = Popup( title="Select Time", content=content, size_hint=(0.8, 0.5), background_color=CARD_BG ) popup.open() self._time_popup = popup def set_time_hour(self, which, hour): if which == 'start': self.trading_start_time = (hour, self.trading_start_time[1]) else: self.trading_stop_time = (hour, self.trading_stop_time[1]) self._update_time_display(which) def set_time_minute(self, which, minute): if which == 'start': self.trading_start_time = (self.trading_start_time[0], minute) else: self.trading_stop_time = (self.trading_stop_time[0], minute) self._update_time_display(which) if hasattr(self, '_time_popup'): self._time_popup.dismiss() def _update_time_display(self, which): if which == 'start': h, m = self.trading_start_time self.start_time_btn.text = f"[b]{h:02d}:{m:02d}[/b]" else: h, m = self.trading_stop_time self.stop_time_btn.text = f"[b]{h:02d}:{m:02d}[/b]" self.update_protocol_info() def show_grade_filter_popup(self, instance): """Show popup for selecting minimum grade filter - with safety checks.""" try: from kivy.uix.popup import Popup from kivy.uix.gridlayout import GridLayout content = BoxLayout(orientation='vertical', padding=dp(10), spacing=dp(8)) content.add_widget(Label(text="[b]Select Minimum Grade[/b]", markup=True, color=GOLD, font_size=sp(14))) grade_grid = GridLayout(cols=4, spacing=dp(6), padding=dp(6)) # Create popup reference first so buttons can reference it popup = Popup( title='Grade Filter', content=content, size_hint=(0.8, 0.5), auto_dismiss=True ) for grade in GRADE_OPTIONS: current_filter = getattr(self, 'min_grade_filter', 'F') btn = StyledButton( text=f"[b]{grade}[/b]", markup=True, bg_color=CYAN if grade == current_filter else CARD_BG, text_color=BLACK if grade == current_filter else WHITE, font_size=sp(11), radius=8 ) btn.bind(on_press=lambda x, g=grade, p=popup: self.set_grade_filter(g, p)) grade_grid.add_widget(btn) content.add_widget(grade_grid) popup.open() except Exception as e: print(f"[Grade Filter] Error showing popup: {e}") self.log_activity(f"[b]ERROR:[/b] Could not open grade filter - {e}", "ERROR") def set_grade_filter(self, grade, popup): """Set the minimum grade filter with safety checks to prevent freeze.""" try: self.min_grade_filter = grade self.min_trade_grade = grade # Safely update button text if hasattr(self, 'grade_filter_btn') and self.grade_filter_btn: self.grade_filter_btn.text = grade # Sync with unified filter mode if grade in ["A+", "A", "A-"]: self.filter_mode = "A_ONLY" elif grade in ["B+", "B", "B-"]: self.filter_mode = "MIN_B" elif grade in ["C+", "C", "C-"]: self.filter_mode = "MIN_C" else: self.filter_mode = "OFF" # Update ROB-BOT tab filter buttons if they exist if hasattr(self, 'filter_mode_buttons') and self.filter_mode_buttons: color_map = { "OFF": RED, "A_ONLY": GREEN, "MIN_B": CYAN, "MIN_C": AMBER } for m, btn in self.filter_mode_buttons.items(): if btn: # Check button exists if m == self.filter_mode: btn.bg_color = color_map.get(m, CARD_BG) btn.text_color = BLACK else: btn.bg_color = CARD_BG btn.text_color = WHITE # Safely dismiss popup try: if popup: popup.dismiss() except Exception as e: print(f"[Grade Filter] Popup dismiss error: {e}") # Log the change self.log_activity(f"[b]GRADE FILTER:[/b] Minimum grade set to {grade}") # Save settings self.save_all_settings() # Trigger automatic refresh after grade change (in background thread to prevent freeze) def auto_refresh_after_grade_change(dt): try: # Clear existing setups if hasattr(self, 'current_setups'): self.current_setups = [] if hasattr(self, 'setups_container') and self.setups_container: self.setups_container.clear_widgets() # Trigger fresh scan in background thread to prevent UI freeze self.log_activity(f"[b]AUTO-REFRESH:[/b] Scanning for {grade}+ setups...") import threading scan_thread = threading.Thread(target=self.force_market_scan, daemon=True) scan_thread.start() except Exception as e: print(f"[Grade Filter] Auto-refresh error: {e}") Clock.schedule_once(auto_refresh_after_grade_change, 0.3) # Slightly longer delay to let popup close except Exception as e: print(f"[Grade Filter] Error in set_grade_filter: {e}") try: if popup: popup.dismiss() except: pass def show_quality_grade_popup(self, instance): from kivy.uix.popup import Popup from kivy.uix.gridlayout import GridLayout content = BoxLayout(orientation='vertical', padding=dp(10), spacing=dp(8)) content.add_widget(Label(text="[b]Select Min Trade Grade[/b]", markup=True, color=GOLD, font_size=sp(14))) content.add_widget(Label(text="Only auto-trade setups with this grade or higher", color=GRAY, font_size=sp(10))) grade_grid = GridLayout(cols=4, spacing=dp(6), padding=dp(6)) trade_grades = ["A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-", "D+", "D", "D-"] for grade in trade_grades: btn = StyledButton( text=f"[b]{grade}[/b]", markup=True, bg_color=GREEN if grade == self.min_trade_grade else CARD_BG, text_color=BLACK if grade == self.min_trade_grade else WHITE, font_size=sp(11), radius=8 ) btn.bind(on_press=lambda x, g=grade: self.set_quality_grade(g, popup)) grade_grid.add_widget(btn) content.add_widget(grade_grid) popup = Popup( title='Trade Quality Filter', content=content, size_hint=(0.8, 0.5), auto_dismiss=True ) popup.open() def set_quality_grade(self, grade, popup): self.min_trade_grade = grade self.min_grade_filter = grade self.quality_grade_btn.text = grade self.grade_filter_btn.text = grade popup.dismiss() self.log_activity(f"[b]QUALITY FILTER:[/b] Auto-trade grade ≥ {grade} (synced with display)") # Save settings self.save_all_settings() def show_grade_selector(self, instance=None): """Show popup to select minimum setup grade from Settings tab.""" from kivy.uix.popup import Popup from kivy.uix.gridlayout import GridLayout content = BoxLayout(orientation='vertical', padding=dp(10), spacing=dp(8)) content.add_widget(Label( text="[b]SELECT MINIMUM GRADE[/b]", markup=True, color=(0.4, 0.5, 0.6, 1), font_size=sp(14) )) content.add_widget(Label( text="Show setups with this grade or higher in SETUPS tab", color=(0.6, 0.6, 0.6, 1), font_size=sp(10) )) grade_grid = GridLayout(cols=4, spacing=dp(6), padding=dp(6)) current_grade = getattr(self, 'min_grade_filter', 'F') for grade in GRADE_OPTIONS: # Color coding: F = gray, D-C = orange, B = blue, A = green if grade == 'F': bg_color = (0.4, 0.4, 0.4, 1) elif grade in ['D+', 'D', 'D-', 'C+', 'C', 'C-']: bg_color = (0.7, 0.5, 0.2, 1) elif grade in ['B+', 'B', 'B-']: bg_color = (0.2, 0.4, 0.7, 1) else: # A grades bg_color = (0.2, 0.6, 0.3, 1) is_selected = (grade == current_grade) btn = StyledButton( text=f"[b]{grade}[/b]", markup=True, bg_color=bg_color, text_color=(0, 0, 0, 1) if is_selected else (1, 1, 1, 1), font_size=sp(11), radius=8 ) # Highlight selected if is_selected: btn.text = f"[b]✓ {grade}[/b]" btn.bind(on_press=lambda x, g=grade: self._set_grade_from_settings(g, popup)) grade_grid.add_widget(btn) content.add_widget(grade_grid) # Info text info_text = { 'F': "Show ALL grades (A+ to F)", 'C': "Show C and above (excludes D/F)", 'B': "Show B and above (quality setups)", 'A': "Show only A grades (best only)" } self._grade_info_popup_lbl = Label( text=info_text.get(current_grade, f"Show {current_grade} and above"), color=(0.5, 0.5, 0.5, 1), font_size=sp(10) ) content.add_widget(self._grade_info_popup_lbl) popup = Popup( title='Grade Filter', content=content, size_hint=(0.85, 0.55), auto_dismiss=True ) popup.open() def _set_grade_from_settings(self, grade, popup): """Set grade filter from settings popup.""" self.min_grade_filter = grade self.min_trade_grade = grade # Update settings tab button if hasattr(self, 'min_grade_btn'): self.min_grade_btn.text = f"[b]{grade}[/b]" # Update info label info_text = { 'F': "F = Show all grades (A+ to F)", 'C': "C = Show C and above (excludes D/F)", 'B': "B = Show B and above (quality setups)", 'A': "A = Show only A grades (best only)", 'A+': "A+ = Show only A+ grades (cream of the crop)" } if hasattr(self, 'grade_info_lbl'): self.grade_info_lbl.text = f"[color=888888]{info_text.get(grade, f'{grade} = Show {grade} and above')}[/color]" popup.dismiss() self.log_activity(f"[b]SETTINGS:[/b] Minimum grade set to {grade}") self.save_all_settings() def set_filter_mode(self, mode): """Set unified filter mode for both SETUPS and ROB-BOT""" self.filter_mode = mode # Map filter mode to min grade grade_map = { "OFF": "F", "A_ONLY": "A", "MIN_B": "B", "MIN_C": "C" } min_grade = grade_map.get(mode, "F") self.min_grade_filter = min_grade self.min_trade_grade = min_grade # Update button colors (if buttons exist) if not hasattr(self, 'filter_mode_buttons') or self.filter_mode_buttons is None: print(f"[FILTER MODE] Buttons not ready yet, mode={mode} saved") return for m, btn in self.filter_mode_buttons.items(): if m == mode: # Keep the button's original color color_map = { "OFF": RED, "A_ONLY": GREEN, "MIN_B": CYAN, "MIN_C": AMBER } btn.bg_color = color_map.get(m, CARD_BG) btn.text_color = BLACK else: btn.bg_color = CARD_BG btn.text_color = WHITE # Update SETUPS tab filter button if hasattr(self, 'grade_filter_btn'): self.grade_filter_btn.text = f"[b]{min_grade}[/b]" # Update info label mode_desc = { "OFF": "No filters | All grades allowed", "A_ONLY": "A/A+ setups only | High quality", "MIN_B": "Min Grade B | A or B setups", "MIN_C": "Min Grade C | A, B, or C setups" } if hasattr(self, 'filter_info_label'): self.filter_info_label.text = f"[color=888888]{mode_desc.get(mode, '')} | R/R ≥ {self.min_rr_ratio}[/color]" # Refresh setups display if hasattr(self, 'current_setups'): self.refresh_setups_display() self.log_activity(f"[b]FILTER MODE:[/b] {mode} (Grade ≥ {min_grade})") # Save settings self.save_all_settings() def show_quality_rr_popup(self, instance): from kivy.uix.popup import Popup content = BoxLayout(orientation='vertical', padding=dp(10), spacing=dp(8)) content.add_widget(Label(text="[b]Select Min R/R Ratio[/b]", markup=True, color=GOLD, font_size=sp(14))) content.add_widget(Label(text="Only auto-trade setups with R/R ≥ this value", color=GRAY, font_size=sp(10))) rr_options = [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 5.0] rr_grid = GridLayout(cols=4, spacing=dp(6), padding=dp(6)) for rr in rr_options: btn = StyledButton( text=f"[b]1:{rr:.1f}[/b]", markup=True, bg_color=GREEN if abs(rr - self.min_rr_ratio) < 0.1 else CARD_BG, text_color=BLACK if abs(rr - self.min_rr_ratio) < 0.1 else WHITE, font_size=sp(11), radius=8 ) btn.bind(on_press=lambda x, r=rr: self.set_quality_rr(r, popup)) rr_grid.add_widget(btn) content.add_widget(rr_grid) popup = Popup( title='R/R Filter', content=content, size_hint=(0.8, 0.4), auto_dismiss=True ) popup.open() def set_quality_rr(self, rr, popup): self.min_rr_ratio = rr self.quality_rr_btn.text = f"{rr:.1f}" popup.dismiss() self.log_activity(f"[b]QUALITY FILTER:[/b] Auto-trade R/R ≥ 1:{rr:.1f}") # Save settings self.save_all_settings() def show_rr_adjust_popup(self, instance=None): """Show popup to adjust R/R for the current top setup""" from kivy.uix.popup import Popup from kivy.uix.slider import Slider # Get current setup R/R current_rr = 1.5 if hasattr(self, '_current_top_setup') and self._current_top_setup: current_rr = self._current_top_setup.get('rr_ratio', 1.5) content = BoxLayout(orientation='vertical', padding=dp(10), spacing=dp(8)) content.add_widget(Label(text="[b]Adjust R/R Ratio[/b]", markup=True, color=GOLD, font_size=sp(14))) content.add_widget(Label(text="Set custom Risk/Reward for the next trade", color=GRAY, font_size=sp(10))) # Current value display self._rr_adjust_value = current_rr rr_value_label = Label( text=f"[b]1:{current_rr:.1f}[/b]", markup=True, color=CYAN, font_size=sp(20) ) content.add_widget(rr_value_label) # Slider for R/R rr_slider = Slider( min=1.0, max=5.0, value=current_rr, step=0.1, size_hint_y=None, height=dp(40) ) def on_rr_value_change(inst, value): self._rr_adjust_value = value rr_value_label.text = f"[b]1:{value:.1f}[/b]" rr_slider.bind(value=on_rr_value_change) content.add_widget(rr_slider) # Preset buttons preset_row = BoxLayout(size_hint_y=None, height=dp(40), spacing=dp(6)) for preset in [1.0, 1.5, 2.0, 2.5, 3.0]: btn = StyledButton( text=f"[b]1:{preset:.1f}[/b]", markup=True, bg_color=CYAN if abs(preset - current_rr) < 0.1 else CARD_BG, text_color=BLACK if abs(preset - current_rr) < 0.1 else WHITE, font_size=sp(10), radius=6 ) btn.bind(on_press=lambda x, p=preset: rr_slider.setter('value')(rr_slider, p)) preset_row.add_widget(btn) content.add_widget(preset_row) # Confirmation buttons btn_row = BoxLayout(size_hint_y=None, height=dp(45), spacing=dp(8)) cancel_btn = StyledButton( text="[b]CANCEL[/b]", markup=True, bg_color=GRAY, text_color=WHITE, font_size=sp(11), radius=8 ) confirm_btn = StyledButton( text="[b]CONFIRM R/R[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(11), radius=8 ) btn_row.add_widget(cancel_btn) btn_row.add_widget(confirm_btn) content.add_widget(btn_row) popup = Popup( title='Set R/R Ratio', content=content, size_hint=(0.85, 0.5), auto_dismiss=False ) def on_confirm(x): # Apply the new R/R to current setup if hasattr(self, '_current_top_setup') and self._current_top_setup: old_rr = self._current_top_setup.get('rr_ratio', 1.5) self._current_top_setup['rr_ratio'] = self._rr_adjust_value # Recalculate TP based on new R/R entry = self._current_top_setup.get('entry', 0) sl = self._current_top_setup.get('stop_loss', 0) direction = self._current_top_setup.get('direction', 'LONG') if entry > 0 and sl > 0: risk = abs(entry - sl) new_tp = entry + (risk * self._rr_adjust_value) if direction == 'LONG' else entry - (risk * self._rr_adjust_value) self._current_top_setup['take_profit1'] = round(new_tp, 2) self.log_activity(f"[b]R/R ADJUSTED:[/b] 1:{old_rr:.1f} → 1:{self._rr_adjust_value:.1f}") self.update_best_setup_display() # Refresh display # Update display label self.rr_display_label.text = f"[b]1:{self._rr_adjust_value:.1f}[/b]" popup.dismiss() def on_cancel(x): popup.dismiss() confirm_btn.bind(on_press=on_confirm) cancel_btn.bind(on_press=on_cancel) popup.open() def clear_bot_log(self, instance): self.bot_log.clear_widgets() self.bot_log.height = 0 self.log_activity("Log cleared") def clear_setups(self, instance): self.current_setups = [] self.setups_container.clear_widgets() self.setups_status_lbl.text = "Setups cleared" self.setups_status_lbl.color = GRAY if hasattr(self, 'best_setup_content'): self.best_setup_content.text = "No setups available." self.log_activity("Setups cleared") def export_bot_log(self, instance=None): try: timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"bot_log_{timestamp}.txt" filepath = os.path.join(DATA_DIR, filename) with open(filepath, 'w', encoding='utf-8') as f: f.write(f"ROB-BOT v52 - BOT LOG EXPORT\n") f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"Mode: {'PAPER' if self.paper_mode else 'LIVE'}\n") f.write(f"Bot Status: {'ENGAGED' if self.bot_engaged else 'STANDBY'}\n") f.write("=" * 60 + "\n\n") log_entries = [] for child in reversed(self.bot_log.children): if hasattr(child, 'text') and child.text.strip(): text = child.text text = text.replace('[b]', '').replace('[/b]', '') text = text.replace('[/color]', '') import re text = re.sub(r'\[color=[^\]]+\]', '', text) log_entries.append(text) for entry in reversed(log_entries): f.write(entry + "\n") f.write("\n" + "=" * 60 + "\n") f.write(f"End of log - {len(log_entries)} entries\n") self.log_activity(f"[b]EXPORTED:[/b] Bot log saved to {filename}", "INFO") self.show_popup("Export Success", f"Bot log exported to:\n{filepath}\n\nYou can now share this file.") return filepath except Exception as e: self.log_activity(f"[b]EXPORT ERROR:[/b] {e}", "ERROR") self.show_popup("Export Error", f"Failed to export log:\n{str(e)}") return None def export_setup_log(self, instance=None): try: timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"setup_log_{timestamp}.txt" filepath = os.path.join(DATA_DIR, filename) with open(filepath, 'w', encoding='utf-8') as f: f.write(f"ROB-BOT v52 - SETUP LOG EXPORT\n") f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"Total Setups Recorded: {len(self.setup_history)}\n") f.write("=" * 80 + "\n\n") for setup in self.setup_history: f.write(f"[{setup.get('timestamp', 'Unknown')}] ") f.write(f"{setup.get('pair', 'Unknown')} ") f.write(f"{setup.get('direction', 'Unknown')} | ") f.write(f"Grade: {setup.get('grade', 'N/A')} | ") f.write(f"Score: {setup.get('score', 0)} | ") f.write(f"R/R: 1:{setup.get('rr_ratio', 0):.2f}\n") f.write(f" Entry: ${setup.get('entry', 0):.2f} | ") f.write(f"SL: ${setup.get('stop_loss', 0):.2f} | ") f.write(f"TP1: ${setup.get('take_profit1', 0):.2f} | ") f.write(f"TP2: ${setup.get('take_profit2', 0):.2f}\n") f.write(f" Type: {setup.get('setup_type', 'STANDARD')}\n") if setup.get('warnings'): f.write(f" Warnings: {', '.join(setup.get('warnings', []))}\n") f.write("-" * 60 + "\n") f.write("\n" + "=" * 80 + "\n") f.write(f"End of setup log\n") self.log_activity(f"[b]EXPORTED:[/b] Setup log saved to {filename}", "INFO") self.show_popup("Export Success", f"Setup log exported to:\n{filepath}\n\nYou can now share this file.") return filepath except Exception as e: self.log_activity(f"[b]EXPORT ERROR:[/b] {e}", "ERROR") self.show_popup("Export Error", f"Failed to export setup log:\n{str(e)}") return None def log_setup_found(self, setup): if not setup or not setup.get('detected'): return if getattr(self, 'silent_mode', False): return setup_record = { 'timestamp': datetime.now().strftime('%H:%M:%S'), 'date': datetime.now().strftime('%Y-%m-%d'), 'pair': setup.get('pair', 'Unknown'), 'direction': setup.get('direction', 'Unknown'), 'grade': setup.get('grade', 'F'), 'score': setup.get('score', 0), 'rr_ratio': setup.get('rr_ratio', 0), 'entry': setup.get('entry', 0), 'stop_loss': setup.get('stop_loss', 0), 'take_profit1': setup.get('take_profit1', 0), 'take_profit2': setup.get('take_profit2', 0), 'setup_type': setup.get('setup_type', 'STANDARD'), 'warnings': setup.get('warnings', []), 'exhaustion_score': setup.get('exhaustion_score', 0), 'atr': setup.get('atr', 0) } self.setup_history.append(setup_record) if len(self.setup_history) > self.max_setup_history: self.setup_history = self.setup_history[-self.max_setup_history:] def log_asset_snapshot(self): try: stats = self.position_manager.get_stats() open_positions = len([p for p in self.position_manager.positions if p['status'] == 'OPEN']) realized_pnl = stats.get('total_pnl', 0) unrealized_pnl = stats.get('unrealized_pnl', 0) total_pnl = realized_pnl + unrealized_pnl current_asset = self.total_asset + total_pnl snapshot = { 'timestamp': datetime.now().strftime('%H:%M:%S'), 'date': datetime.now().strftime('%Y-%m-%d'), 'total_asset': current_asset, 'starting_asset': self.total_asset, 'realized_pnl': realized_pnl, 'unrealized_pnl': unrealized_pnl, 'total_pnl': total_pnl, 'growth_pct': (total_pnl / self.total_asset * 100) if self.total_asset > 0 else 0, 'open_positions': open_positions, 'win_rate': stats.get('win_rate', 0), 'total_trades': stats.get('total_trades', 0) } self.asset_log.append(snapshot) if len(self.asset_log) > self.max_asset_log: self.asset_log = self.asset_log[-self.max_asset_log:] except Exception as e: print(f"Asset log error: {e}") def export_asset_log(self, instance=None): try: timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"asset_log_{timestamp}.txt" filepath = os.path.join(DATA_DIR, filename) with open(filepath, 'w', encoding='utf-8') as f: f.write(f"ROB-BOT v52 - ASSET GROWTH LOG\n") f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"Starting Balance: ${self.total_asset:,.2f}\n") f.write("=" * 80 + "\n\n") if not self.asset_log: f.write("No asset data recorded yet.\n") f.write("Asset snapshots are taken every 5 minutes during bot operation.\n") else: f.write(f"{'Time':<10} {'Asset':<12} {'Realized':<12} {'Unrealized':<12} {'Total PnL':<12} {'Growth%':<10} {'Trades':<8}\n") f.write("-" * 80 + "\n") for entry in self.asset_log: f.write(f"{entry['timestamp']:<10} ") f.write(f"${entry['total_asset']:<11,.2f} ") f.write(f"${entry['realized_pnl']:<11,.2f} ") f.write(f"${entry['unrealized_pnl']:<11,.2f} ") f.write(f"${entry['total_pnl']:<11,.2f} ") f.write(f"{entry['growth_pct']:<9.2f}% ") f.write(f"{entry['total_trades']:<8}\n") f.write("\n" + "=" * 80 + "\n") if len(self.asset_log) >= 2: first = self.asset_log[0] last = self.asset_log[-1] f.write(f"\nSUMMARY:\n") f.write(f" Period: {first['date']} {first['timestamp']} to {last['date']} {last['timestamp']}\n") f.write(f" Starting: ${first['total_asset']:,.2f}\n") f.write(f" Current: ${last['total_asset']:,.2f}\n") f.write(f" Total Growth: ${last['total_pnl'] - first['total_pnl']:,.2f}\n") f.write(f" Win Rate: {last['win_rate']:.1f}%\n") f.write("\n" + "=" * 80 + "\n") f.write(f"End of asset log - {len(self.asset_log)} entries\n") self.log_activity(f"[b]EXPORTED:[/b] Asset log saved to {filename}", "INFO") self.show_popup("Export Success", f"Asset growth log exported to:\n{filepath}\n\nTrack your PnL and growth over time!") return filepath except Exception as e: self.log_activity(f"[b]EXPORT ERROR:[/b] {e}", "ERROR") self.show_popup("Export Error", f"Failed to export asset log:\n{str(e)}") return None def create_report(self, instance): stats = self.position_manager.get_stats() report = f"[b]Trading Report[/b]\n\n" report += f"Total Trades: {stats['total_trades']}\n" report += f"Win Rate: {stats['win_rate']:.1f}%\n" report += f"Total PnL: ${stats['total_pnl']:+.2f}\n" report += f"Open Positions: {stats['open_positions']}\n\n" open_positions = [p for p in self.position_manager.positions if p['status'] == 'OPEN'] if open_positions: report += "[b]Open Positions:[/b]\n" for pos in open_positions: unrealized = pos.get('unrealized_pnl', 0) report += f" {pos['pair']} {pos['direction']} | Entry: ${pos['entry_price']:.2f} | PnL: ${unrealized:+.2f}\n" else: report += "No open positions" self.show_popup("Report", report) self.log_activity("Report generated") def run_speed_test(self, instance=None): """Run high-speed backtest on historical data""" if not BACKTEST_AVAILABLE: self.show_popup("Error", "Backtest module not available.\nMake sure rob_bot_backtest.py is in the same folder.") return self.log_activity("[b]BACKTEST:[/b] Starting speed test...", "INFO") def do_test(): try: # Test top 5 pairs for last 30 days pairs = self.enabled_pairs[:5] self.log_activity(f"[b]BACKTEST:[/b] Testing {len(pairs)} pairs...", "INFO") # Run the test results = run_speed_test(self.binance, pairs, days=30) if results: # Calculate aggregate stats total_trades = sum(r['stats']['total_trades'] for r in results) avg_tp1 = sum(r['stats']['tp1_hit_rate'] for r in results) / len(results) avg_pf = sum(r['stats']['profit_factor'] for r in results) / len(results) total_pnl = sum(r['stats']['net_pnl'] for r in results) avg_rr = sum(r['stats']['avg_r_multiple'] for r in results) / len(results) # Show results on main thread def show_results(): result_msg = ( f"[b]SPEED TEST COMPLETE[/b]\n\n" f"Pairs Tested: {len(results)}\n" f"Total Setups: {total_trades}\n" f"Avg TP1 Hit: {avg_tp1:.1f}%\n" f"Avg Win Rate: {sum(r['stats']['win_rate'] for r in results) / len(results):.1f}%\n" f"Avg R:R: {avg_rr:.2f}:1\n" f"Avg Profit Factor: {avg_pf:.2f}\n" f"Total PnL: ${total_pnl:.2f}" ) self.log_activity(f"[b]BACKTEST:[/b] Complete - {total_trades} setups, {avg_tp1:.0f}% TP1", "INFO") self.show_popup("Backtest Results", result_msg) Clock.schedule_once(lambda dt: show_results(), 0) else: Clock.schedule_once(lambda dt: self.show_popup("Error", "No results from backtest"), 0) except Exception as e: def show_error(): self.log_activity(f"[b]BACKTEST ERROR:[/b] {str(e)}", "ERROR") self.show_popup("Backtest Error", str(e)) Clock.schedule_once(lambda dt: show_error(), 0) # Run in background thread threading.Thread(target=do_test, daemon=True).start() def toggle_data_logging(self, instance=None): """Toggle live data collection for optimization analysis""" self.data_logging_enabled = not self.data_logging_enabled if self.data_logging_enabled: instance.text = "[b]LIVE LOG: ON[/b]" instance.bg_color = GREEN self.log_activity("[b]DATA LOG:[/b] Live market data collection ENABLED", "INFO") self.show_popup("Data Logging Enabled", "Market data will be logged every 60 seconds.\n\n" "This data helps optimize:\n" "- Entry point timing\n" "- TP1/TP2 levels\n" "- Trailing stop behavior\n" "- SL placement") else: instance.text = "[b]LIVE LOG: OFF[/b]" instance.bg_color = GRAY self.log_activity("[b]DATA LOG:[/b] Live market data collection DISABLED", "INFO") # Export collected data if self.live_data_log: self._export_live_data() def _export_live_data(self): """Export collected live data to CSV for analysis""" try: import csv import os filepath = os.path.join(DATA_DIR, 'live_data_log.csv') with open(filepath, 'w', newline='') as f: if self.live_data_log: writer = csv.DictWriter(f, fieldnames=self.live_data_log[0].keys()) writer.writeheader() writer.writerows(self.live_data_log) self.log_activity(f"[b]DATA LOG:[/b] Exported {len(self.live_data_log)} entries to live_data_log.csv", "INFO") self.live_data_log = [] # Clear after export except Exception as e: self.log_activity(f"[b]DATA LOG ERROR:[/b] {e}", "ERROR") def toggle_smart_trailing(self, instance=None): """Toggle smart order book based trailing stops""" self.use_smart_trailing = not self.use_smart_trailing if self.use_smart_trailing: instance.text = "[b]ORDER BOOK TRAIL: ON[/b]" instance.bg_color = GREEN self.log_activity("[b]SMART TRAIL:[/b] Order book based trailing ENABLED", "INFO") self.show_popup("Smart Trailing Enabled", "Trailing stops will now use order book analysis:\n\n" "- Real support/resistance walls detected\n" "- Spoof walls filtered out\n" "- Trails placed at actual liquidity levels\n" "- Falls back to % trails if no clear walls") else: instance.text = "[b]ORDER BOOK TRAIL: OFF[/b]" instance.bg_color = GRAY self.log_activity("[b]SMART TRAIL:[/b] Order book based trailing DISABLED - using % trails", "INFO") def _log_market_data(self): """Log current market state for optimization analysis""" if not self.data_logging_enabled: return current_time = time.time() if current_time - self.last_data_log < self.data_log_interval: return self.last_data_log = current_time try: for pair in self.enabled_pairs[:10]: # Log top 10 pairs if pair in self.market_data: data = self.market_data[pair] log_entry = { 'timestamp': datetime.now().isoformat(), 'pair': pair, 'price': data.get('price', 0), 'volume_24h': data.get('volume_24h', 0), 'price_change_24h': data.get('price_change_24h', 0), 'open_positions': len([p for p in self.position_manager.positions if p['status'] == 'OPEN' and p['pair'] == pair]), 'unrealized_pnl': sum(p.get('unrealized_pnl', 0) for p in self.position_manager.positions if p['pair'] == pair and p['status'] == 'OPEN'), } self.live_data_log.append(log_entry) # Trim if too large if len(self.live_data_log) > self.max_live_data_entries: self.live_data_log = self.live_data_log[-self.max_live_data_entries:] except Exception as e: print(f"[Data Log Error] {e}") def optimize_bot_params(self, instance=None): """Run parameter optimization on best pair""" if not BACKTEST_AVAILABLE: self.show_popup("Error", "Backtest module not available.") return self.log_activity("[b]OPTIMIZE:[/b] Starting parameter grid search...", "INFO") def do_optimize(): try: # Use first enabled pair pair = self.enabled_pairs[0] if self.enabled_pairs else "BTCUSDC" self.log_activity(f"[b]OPTIMIZE:[/b] Testing {pair}...", "INFO") # Run optimization results = optimize_for_profit_factor(self.binance, pair, days=60) best = results['best_params'] stats = results['best_result']['stats'] # Apply to bot self.tp1_rr_multiplier = best['tp1_multiplier'] self.tp2_rr_multiplier = best['tp2_multiplier'] self.sl_atr_multiplier = best['sl_atr_multiplier'] def show_results(): msg = ( f"[b]OPTIMIZATION COMPLETE[/b]\n\n" f"Pair: {pair}\n" f"Best TP1: {best['tp1_multiplier']}x\n" f"Best TP2: {best['tp2_multiplier']}x\n" f"Best SL: {best['sl_atr_multiplier']}x ATR\n\n" f"Expected TP1 Hit: {stats['tp1_hit_rate']:.1f}%\n" f"Expected Win Rate: {stats['win_rate']:.1f}%\n" f"Expected R:R: {stats['avg_r_multiple']:.2f}:1\n" f"Expected PF: {stats['profit_factor']:.2f}\n\n" f"[color=33a633]Parameters applied automatically![/color]" ) self.log_activity(f"[b]OPTIMIZE:[/b] Params updated - TP1:{best['tp1_multiplier']}x, SL:{best['sl_atr_multiplier']}x", "INFO") self.show_popup("Optimization Complete", msg) Clock.schedule_once(lambda dt: show_results(), 0) except Exception as e: def show_error(): self.log_activity(f"[b]OPTIMIZE ERROR:[/b] {str(e)}", "ERROR") self.show_popup("Optimization Error", str(e)) Clock.schedule_once(lambda dt: show_error(), 0) threading.Thread(target=do_optimize, daemon=True).start() def reset_pnl_callback(self, instance): """Reset P&L with confirmation dialog.""" # Show confirmation dialog content = BoxLayout(orientation='vertical', spacing=dp(10), padding=dp(20)) content.add_widget(Label( text="[b]Reset ALL P&L counters?[/b]\n\nThis will clear:\n- Daily P&L\n- Weekly P&L\n- Total P&L\n- Consecutive losses\n\nThis action cannot be undone!", markup=True, color=WHITE, font_size=sp(11) )) btn_box = BoxLayout(spacing=dp(10), size_hint_y=0.4) yes_btn = StyledButton(text="[b]RESET P&L[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(11), radius=10) no_btn = StyledButton(text="[b]CANCEL[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(11), radius=10) popup = Popup(title="Confirm P&L Reset", content=content, size_hint=(0.8, 0.55), background_color=CARD_BG) def do_reset(): popup.dismiss() self._execute_pnl_reset() def cancel(): popup.dismiss() yes_btn.bind(on_press=lambda x: do_reset()) no_btn.bind(on_press=lambda x: cancel()) btn_box.add_widget(yes_btn) btn_box.add_widget(no_btn) content.add_widget(btn_box) popup.open() def _reset_pnl_manual(self, instance=None): """Manually reset PnL from positions tab with confirmation.""" content = BoxLayout(orientation='vertical', spacing=dp(10), padding=dp(20)) content.add_widget(Label( text="[b]Reset P&L?[/b]\n\nThis will reset:\n- Daily P&L\n- Weekly P&L\n- Total P&L\n\nUse this if values appear corrupted.", markup=True, color=WHITE, font_size=sp(11) )) btn_box = BoxLayout(spacing=dp(10), size_hint_y=0.4) yes_btn = StyledButton(text="[b]RESET[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(11), radius=10) no_btn = StyledButton(text="[b]CANCEL[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(11), radius=10) popup = Popup(title="Confirm Reset", content=content, size_hint=(0.8, 0.5), background_color=CARD_BG) def do_reset(): self._execute_pnl_reset() popup.dismiss() yes_btn.bind(on_press=lambda x: do_reset()) no_btn.bind(on_press=popup.dismiss) btn_box.add_widget(yes_btn) btn_box.add_widget(no_btn) content.add_widget(btn_box) popup.open() def _execute_pnl_reset(self): """Execute the actual P&L reset after confirmation.""" self.position_manager.reset_pnl() if hasattr(self, 'circuit_breaker'): self.circuit_breaker.daily_start_pnl = 0 self.circuit_breaker.weekly_start_pnl = 0 self.circuit_breaker.consecutive_losses = 0 # Update P&L labels (now in Assets tab) - check if they exist if hasattr(self, 'daily_pnl_lbl'): self.daily_pnl_lbl.text = "Daily: $0" if hasattr(self, 'weekly_pnl_lbl'): self.weekly_pnl_lbl.text = "Weekly: $0" if hasattr(self, 'total_pnl_lbl'): self.total_pnl_lbl.text = "Total: $0" self.log_activity("[b]P&L RESET:[/b] All profit/loss counters cleared", "WARN") self.show_popup("[b]P&L RESET[/b]", "All P&L counters have been reset to zero.\n\nDaily: $0\nWeekly: $0\nTotal: $0") def _auto_save_setting(self, instance): """Auto-save settings when text input is validated (Enter pressed).""" try: self.save_settings_ui(None) print(f"[AUTO-SAVE] Setting saved from {instance}") except Exception as e: print(f"[AUTO-SAVE ERROR] {e}") def save_settings_ui(self, inst, show_popup=True): """Save settings with Robot Scanner configuration. Args: inst: The widget instance that triggered the save (can be None) show_popup: If True, shows success popup. Set to False for silent saves (auto-save). """ try: # Basic settings self.total_asset = float(self.asset_input.text) self.risk_per_trade = float(self.risk_input.text) / 100 self.max_simultaneous = int(self.max_trades_input.text) # max_trades_cycle_input was removed in redesign - use max_simultaneous if not present if hasattr(self, 'max_trades_cycle_input'): self.max_trades_per_cycle = int(self.max_trades_cycle_input.text) else: self.max_trades_per_cycle = self.max_simultaneous # Default to max_simultaneous print(f"[SAVE DEBUG] Saving max_simultaneous={self.max_simultaneous} from input '{self.max_trades_input.text}'") # Robot Scanner settings if hasattr(self, 'leverage_input'): self.leverage = int(self.leverage_input.text) self.position_manager.leverage = self.leverage if hasattr(self, 'max_position_pct_input'): self.max_position_pct = float(self.max_position_pct_input.text) / 100 if hasattr(self, 'target_rr_input'): self.target_rr_ratio = float(self.target_rr_input.text) self.min_rr_ratio = self.target_rr_ratio self.position_manager.target_rr = self.target_rr_ratio if hasattr(self, 'compound_toggle'): self.compound_trading = self.compound_toggle.state == 'down' # Update paper mode allocations if self.paper_mode: self.paper_total_asset = self.total_asset self.paper_usdc_allocation = self.total_asset / 2 self.paper_usdt_allocation = self.total_asset / 2 # Recalculate trade sizes self._calculate_trade_sizes() api_key = self.api_key_input.text.strip() api_secret = self.api_secret_input.text.strip() api_updated = False if api_key and api_secret: self.api_key = api_key self.api_secret = api_secret self.binance = BinanceClient(api_key, api_secret, self.paper_mode) api_updated = True # Save all settings using new method self.save_all_settings() # Verify save worked verify_settings = safe_json_load(SETTINGS_FILE, {}) print(f"[SAVE VERIFY] Saved max_simultaneous={verify_settings.get('max_simultaneous')}, expected={self.max_simultaneous}") self.position_manager.risk_per_trade = self.risk_per_trade # Only show popup if requested (not for auto-saves) if show_popup: msg = f"[b]Settings Saved Successfully![/b]\n\n" msg += f"Total Asset: ${self.total_asset:,.2f}\n" msg += f"Risk per Trade: {self.risk_per_trade*100:.1f}%\n" msg += f"Max Positions: {self.max_simultaneous}\n" msg += f"Leverage: {self.leverage}X\n" msg += f"Max Position: {self.max_position_pct*100:.0f}%\n" msg += f"Target R/R: 1:{self.target_rr_ratio}\n" msg += f"Compound: {'ON' if self.compound_trading else 'OFF'}\n" msg += f"Mode: {'PAPER' if self.paper_mode else 'LIVE'}\n" if api_updated: msg += f"API: Updated\n" msg += f"\n[i]Settings saved to {SETTINGS_FILE}[/i]" self.show_popup("[b]SUCCESS[/b]", msg) self.log_activity("Settings saved successfully") except ValueError as e: if show_popup: self.show_popup("[b]ERROR[/b]", f"Invalid value entered:\n\n{e}\n\nPlease check:\n- Total Asset (number)\n- Risk % (number)\n- Max Trades (whole number)") except Exception as e: if show_popup: self.show_popup("[b]ERROR[/b]", f"Failed to save settings:\n\n{str(e)}") def _export_behavior_log(self, instance=None): """Export behavior log to file.""" try: logger = get_behavior_logger() filepath = logger.export_log() self.log_activity(f"[b]BEHAVIOR LOG:[/b] Exported to {filepath}", "SUCCESS") self.show_popup("[b]LOG EXPORTED[/b]", f"Behavior log saved to:\n{filepath}\n\nTransfer this file for analysis.") except Exception as e: self.log_activity(f"[b]EXPORT ERROR:[/b] {e}", "ERROR") self.show_popup("[b]EXPORT FAILED[/b]", f"Could not export log:\n{e}") def _show_behavior_summary(self, instance=None): """Show behavior summary popup.""" try: logger = get_behavior_logger() summary = logger.get_session_summary() metrics = summary['metrics'] content = f"""[b]SESSION SUMMARY[/b] Duration: {summary['session_duration_minutes']:.1f} minutes Total Events: {summary['total_events']} [b]METRICS:[/b] Scans: {metrics['scans_performed']} Setups Found: {metrics['setups_found']} Setups Filtered: {metrics['setups_filtered']} Trades Attempted: {metrics['trades_attempted']} Trades Executed: {metrics['trades_executed']} Trades Rejected: {metrics['trades_rejected']} Positions Opened: {metrics['positions_opened']} Positions Closed: {metrics['positions_closed']} [b]PROTECTION:[/b] Breakeven Triggered: {metrics['breakeven_triggered']} Profit Locks: {metrics['profit_protection_triggered']} Time Exits: {metrics['time_exits']} """ self.show_popup("[b]BEHAVIOR SUMMARY[/b]", content) except Exception as e: self.log_activity(f"[b]SUMMARY ERROR:[/b] {e}", "ERROR") self.show_popup("[b]SUMMARY FAILED[/b]", f"Could not generate summary:\n{e}") def reset_to_default_settings(self, instance=None): """Reset all settings to Robot Scanner defaults.""" try: # ROBOT SCANNER DEFAULT VALUES defaults = { # Robot Scanner mode 'robot_scanner_mode': True, 'bot_engaged': True, # Paper mode defaults 'paper_mode': True, 'paper_total_asset': 10000.0, 'paper_usdc_allocation': 5000.0, 'paper_usdt_allocation': 5000.0, # Trading parameters 'total_asset': 10000.0, 'risk_per_trade': 0.01, # 1% risk 'target_rr_ratio': 2.5, # 1:2.5 R/R minimum (optimal is 3.0) 'max_position_pct': 0.20, # 20% max per trade 'max_simultaneous': 2, # 2 simultaneous trades 'max_trades_per_cycle': 2, # Leverage 'leverage': 3, # 3X default 'max_leverage': 5, # Smart entry 'smart_entry_enabled': True, 'compound_trading': False, 'compound_pct': 0.50, # TP/SL settings - 75% at TP1 (1.5 R/R min), 25% trails to TP2 'tp1_pct': 0.75, # 75% at TP1 (1.5 R/R minimum profit) 'tp2_pct': 0.25, # 25% continues with trailing to TP2 'trail_pct': 0.005, # 0.5% trailing stop for TP2 'move_sl_to_tp1': True, # Move SL close to TP1 after TP1 hit 'trail_start_pct': 0.10, # Start TP2 trailing 10% above TP1 'sl_buffer_after_tp1': 0.001, # SL buffer after TP1 hit 'trail_method': 'dynamic', # 'dynamic' or 'percent' trailing # API (cleared) 'api_key': '', 'api_secret': '', 'live_trading_configured': False, # Assets 'usdc_balance': 0.0, 'usdt_balance': 0.0, 'usdc_enabled': True, 'usdt_enabled': True, # Scanning 'auto_refresh': True, 'continuous_scan': True, 'auto_scan_interval': 60, 'trade_direction': 'BOTH', 'scan_mode': 'fast', # Sound 'sound_enabled': True, 'sound_theme': 'pro_trader', 'vibration_enabled': True, # Pairs 'quote_currency': 'BOTH', 'enabled_pairs': TOP_PAIRS + TOP_PAIRS_USDT, 'blocked_pairs': [], 'min_trade_grade': 'B', # Circuit breaker 'circuit_breaker_enabled': True, 'circuit_breaker_max_daily_loss': 0.05, # 5% } # Apply to instance variables self.robot_scanner_mode = defaults['robot_scanner_mode'] self.bot_engaged = defaults['bot_engaged'] self.paper_mode = defaults['paper_mode'] self.paper_total_asset = defaults['paper_total_asset'] self.paper_usdc_allocation = defaults['paper_usdc_allocation'] self.paper_usdt_allocation = defaults['paper_usdt_allocation'] self.total_asset = defaults['total_asset'] self.risk_per_trade = defaults['risk_per_trade'] self.target_rr_ratio = defaults['target_rr_ratio'] self.max_position_pct = defaults['max_position_pct'] self.max_simultaneous = defaults['max_simultaneous'] self.max_trades_per_cycle = defaults['max_trades_per_cycle'] self.leverage = defaults['leverage'] self.max_leverage = defaults['max_leverage'] self.smart_entry_enabled = defaults['smart_entry_enabled'] self.compound_trading = defaults['compound_trading'] self.compound_pct = defaults['compound_pct'] self.tp1_pct = defaults['tp1_pct'] self.tp2_pct = defaults['tp2_pct'] self.trail_pct = defaults['trail_pct'] self.move_sl_to_tp1 = defaults['move_sl_to_tp1'] self.api_key = defaults['api_key'] self.api_secret = defaults['api_secret'] self.live_trading_configured = defaults['live_trading_configured'] self.usdc_balance = defaults['usdc_balance'] self.usdt_balance = defaults['usdt_balance'] self.usdc_enabled = defaults['usdc_enabled'] self.usdt_enabled = defaults['usdt_enabled'] self.auto_refresh = defaults['auto_refresh'] self.continuous_scan = defaults['continuous_scan'] self.auto_scan_interval = defaults['auto_scan_interval'] self.trade_direction = defaults['trade_direction'] self.scan_mode = defaults['scan_mode'] self.quote_currency = defaults['quote_currency'] self.enabled_pairs = defaults['enabled_pairs'] self.blocked_pairs = defaults['blocked_pairs'] self.min_trade_grade = defaults['min_trade_grade'] self.min_grade_filter = defaults['min_trade_grade'] self.circuit_breaker_enabled = defaults['circuit_breaker_enabled'] self.circuit_breaker_max_daily_loss = defaults['circuit_breaker_max_daily_loss'] # Update position manager self.position_manager.leverage = self.leverage self.position_manager.max_position_pct = self.max_position_pct self.position_manager.target_rr = self.target_rr_ratio self.position_manager.risk_per_trade = self.risk_per_trade # Recalculate trade sizes self._calculate_trade_sizes() # Save to file safe_json_save(SETTINGS_FILE, defaults) # Update UI self._update_settings_ui() # Show confirmation msg = "[b]Settings Reset to Robot Scanner Defaults![/b]\n\n" msg += f"✓ Paper Mode: ${self.total_asset:,.0f}\n" msg += f"✓ Risk: {self.risk_per_trade*100:.1f}%\n" msg += f"✓ Leverage: {self.leverage}X (max {self.max_leverage}X)\n" msg += f"✓ Max Position: {self.max_position_pct*100:.0f}%\n" msg += f"✓ Target R/R: 1:{self.target_rr_ratio}\n" msg += f"✓ Max Trades: {self.max_simultaneous}\n" msg += f"✓ Quote Currency: {self.quote_currency}\n" msg += f"✓ Robot Scanner: {'ON' if self.robot_scanner_mode else 'OFF'}\n" self.show_popup("[b]DEFAULTS RESTORED[/b]", msg) self.log_activity("Settings reset to Robot Scanner defaults", "INFO") except Exception as e: self.show_popup("[b]ERROR[/b]", f"Failed to reset settings:\n\n{str(e)}") self.max_simultaneous = defaults['max_simultaneous'] self.max_trades_per_cycle = defaults['max_trades_per_cycle'] self.paper_mode = defaults['paper_mode'] self.api_key = defaults['api_key'] self.api_secret = defaults['api_secret'] self.auto_refresh = defaults['auto_refresh'] self.auto_scan_interval = defaults['auto_scan_interval'] self.trade_direction = defaults['trade_direction'] self.scan_mode = defaults['scan_mode'] self.quote_currency = defaults['quote_currency'] self.enabled_pairs = defaults['enabled_pairs'] self.blocked_pairs = defaults['blocked_pairs'] self.min_trade_grade = defaults['min_trade_grade'] self.all_limits_off = defaults['all_limits_off'] self.time_restrictions_enabled = defaults['time_restrictions_enabled'] # Update sound manager if hasattr(self, 'sound_manager'): self.sound_manager.enabled = defaults['sound_enabled'] self.sound_manager.current_theme = defaults['sound_theme'] self.sound_manager.vibration_enabled = defaults['vibration_enabled'] # Update position manager if hasattr(self, 'position_manager'): self.position_manager.risk_per_trade = defaults['risk_per_trade'] # Update UI inputs self.asset_input.text = str(self.total_asset) self.risk_input.text = str(self.risk_per_trade * 100) self.max_trades_input.text = str(self.max_simultaneous) if hasattr(self, 'max_trades_cycle_input'): self.max_trades_cycle_input.text = str(self.max_trades_per_cycle) self.api_key_input.text = '' self.api_secret_input.text = '' # Save to file safe_json_save(SETTINGS_FILE, defaults) # Show confirmation msg = """[b]Settings Reset to Default![/b] [b]Risk Settings:[/b] • Total Asset: $10,000 • Risk per Trade: 2% • Max Simultaneous: 5 • Max per Cycle: 2 [b]Trade Settings:[/b] • Min Grade: B • Direction: BOTH • Paper Mode: ON [b]Time Restrictions:[/b] • [color=00FF00]DISABLED[/color] - Trading allowed 24/7 [i]Settings saved to file[/i]""" self.show_popup("[b]RESET COMPLETE[/b]", msg) self.log_activity("[b]SETTINGS:[/b] Reset to factory defaults (Grade B, No time restrictions)") print("[SETTINGS] Reset to defaults: Grade B, Max 5 simultaneous, 2 per cycle, No time restrictions") except Exception as e: self.show_popup("[b]ERROR[/b]", f"Failed to reset settings:\n\n{str(e)}") print(f"[SETTINGS ERROR] Reset failed: {e}") def load_settings(self): settings = safe_json_load(SETTINGS_FILE, {}) # [SET] Raw settings loaded: {settings}") # [SET] SETTINGS_FILE path: {SETTINGS_FILE}") # ============================================ # ROBOT SCANNER MODE SETTINGS # ============================================ # Robot Scanner mode - auto trade by default self.robot_scanner_mode = settings.get('robot_scanner_mode', True) self.min_score = settings.get('min_score', 75) self.max_trades = settings.get('max_trades', 3) self.bot_engaged = self.robot_scanner_mode # Auto-engage if in robot mode # Paper mode settings self.paper_mode = settings.get('paper_mode', True) self.paper_total_asset = settings.get('paper_total_asset', 10000.0) self.paper_usdc_allocation = settings.get('paper_usdc_allocation', 5000.0) self.paper_usdt_allocation = settings.get('paper_usdt_allocation', 5000.0) # Set total asset based on mode if self.paper_mode: self.total_asset = self.paper_total_asset else: self.total_asset = settings.get('total_asset', 10000.0) # Risk and R/R settings - Reward always bigger than risk: 1% risk, 2.5% reward minimum (optimal 3.0%) self.risk_per_trade = settings.get('risk_per_trade', 0.01) # 1% risk default self.target_rr_ratio = settings.get('target_rr_ratio', 2.5) # 1:2.5 R/R minimum (optimal is 3.0) self.min_rr_ratio = self.target_rr_ratio # Position sizing - max 20% per trade self.max_position_pct = settings.get('max_position_pct', 0.20) self.max_simultaneous = settings.get('max_simultaneous', 2) # 2 trades default self.max_trades_per_cycle = settings.get('max_trades_per_cycle', 2) # [SET] Loaded max_simultaneous={self.max_simultaneous} from settings file") # Leverage settings (3X default, 5X max) self.leverage = settings.get('leverage', 3) self.max_leverage = settings.get('max_leverage', 5) # Update position manager leverage if hasattr(self, 'position_manager'): self.position_manager.leverage = self.leverage # Smart entry and compound trading self.smart_entry_enabled = settings.get('smart_entry_enabled', True) self.compound_trading = settings.get('compound_trading', False) self.compound_pct = settings.get('compound_pct', 0.50) # TP/SL Settings - 75% at TP1 (1.5 R/R min), 25% trails to TP2 self.tp1_pct = settings.get('tp1_pct', 0.75) # 75% at TP1 (1.5 R/R minimum) self.tp2_pct = settings.get('tp2_pct', 0.25) # 25% continues with trailing self.trail_pct = settings.get('trail_pct', 0.005) # 0.5% trail for TP2 self.move_sl_to_tp1 = settings.get('move_sl_to_tp1', True) # Move SL close to TP1 after TP1 hit self.trail_start_pct = settings.get('trail_start_pct', 0.10) # Start TP2 trailing 10% above TP1 self.sl_buffer_after_tp1 = settings.get('sl_buffer_after_tp1', 0.001) # SL buffer after TP1 hit self.trail_method = settings.get('trail_method', 'dynamic') # 'dynamic' or 'percent' # Auto-refresh settings self.auto_refresh = settings.get('auto_refresh', True) self.continuous_scan = settings.get('continuous_scan', True) self.auto_scan_interval = settings.get('auto_scan_interval', 60) # 1 minute self.trade_direction = settings.get('trade_direction', 'BOTH') self.scan_mode = settings.get('scan_mode', 'fast') # RESPECT OFF SWITCH - Load user disabled flags self.optimus_user_disabled = settings.get('optimus_user_disabled', False) self.big_brain_user_disabled = settings.get('big_brain_user_disabled', False) if self.optimus_user_disabled: print("[SETTINGS] Optimus was disabled by user - auto-start blocked") if self.big_brain_user_disabled: print("[SETTINGS] Big Brain was disabled by user - auto-start blocked") # ============================================ # SECURITY & API SETTINGS # ============================================ # API Keys (encrypted storage) self.api_key = settings.get('api_key', '') self.api_secret = settings.get('api_secret', '') self.api_key_encrypted = settings.get('api_key_encrypted', False) # User authentication self.user_password_hash = settings.get('user_password_hash', None) self.user_email = settings.get('user_email', '') self.user_phone = settings.get('user_phone', '') self.google_auth_enabled = settings.get('google_auth_enabled', False) self.google_auth_secret = settings.get('google_auth_secret', '') # Live trading configuration status self.live_trading_configured = settings.get('live_trading_configured', False) # ============================================ # ASSET CONFIGURATION (Live Trading) # ============================================ # Asset balances and settings self.usdc_balance = settings.get('usdc_balance', 0.0) self.usdt_balance = settings.get('usdt_balance', 0.0) self.usdc_enabled = settings.get('usdc_enabled', True) self.usdt_enabled = settings.get('usdt_enabled', True) # Calculate trade sizes based on max_position_pct self._calculate_trade_sizes() # Circuit breaker settings self.circuit_breaker_enabled = settings.get('circuit_breaker_enabled', True) self.circuit_breaker_max_daily_loss = settings.get('circuit_breaker_max_daily_loss', 0.05) # 5% # Load sound settings self.sound_settings = { 'enabled': settings.get('sound_enabled', True), 'theme': settings.get('sound_theme', 'pro_trader'), 'vibration': settings.get('vibration_enabled', True) } # Grade filter settings self.min_trade_grade = settings.get('min_trade_grade', 'B') self.min_grade_filter = self.min_trade_grade # Time restriction settings self.time_restrictions_enabled = settings.get('time_restrictions_enabled', False) self.trading_start_hour = settings.get('trading_start_hour', 0) self.trading_end_hour = settings.get('trading_end_hour', 23) # ============================================ # PAIR CONFIGURATION BASED ON ASSETS # ============================================ # Set default pairs based on enabled assets if not self.paper_mode and self.live_trading_configured: # Live mode: use pairs based on actual asset balances if self.usdc_enabled and self.usdt_enabled: self.quote_currency = "BOTH" default_pairs = TOP_PAIRS.copy() + TOP_PAIRS_USDT.copy() elif self.usdc_enabled: self.quote_currency = "USDC" default_pairs = TOP_PAIRS.copy() elif self.usdt_enabled: self.quote_currency = "USDT" default_pairs = TOP_PAIRS_USDT.copy() else: # Fallback to USDC if no assets enabled self.quote_currency = "USDC" default_pairs = TOP_PAIRS.copy() else: # Paper mode: use BOTH by default (one USDC, one USDT trade) self.quote_currency = settings.get('quote_currency', 'BOTH') default_pairs = TOP_PAIRS.copy() + TOP_PAIRS_USDT.copy() if self.quote_currency == 'BOTH' else ( TOP_PAIRS.copy() if self.quote_currency == 'USDC' else TOP_PAIRS_USDT.copy() ) self.enabled_pairs = settings.get('enabled_pairs', default_pairs) self.blocked_pairs = settings.get('blocked_pairs', []) # [SET] blocked_pairs loaded: {self.blocked_pairs}") # Auto-clear blocked pairs if blocking too many usdt_in_blocked = [p for p in self.blocked_pairs if 'USDT' in p] if len(usdt_in_blocked) > 5: print(f"[SETTINGS FIX] WARNING: {len(usdt_in_blocked)} USDT pairs in blocked_pairs!") print(f"[SETTINGS FIX] Auto-clearing blocked_pairs to allow USDT scanning") self.blocked_pairs = [] settings['blocked_pairs'] = [] safe_json_save(SETTINGS_FILE, settings) # Validate pairs based on quote currency if self.quote_currency == "USDT": valid_pairs = set(TOP_PAIRS_USDT) elif self.quote_currency == "BOTH": valid_pairs = set(TOP_PAIRS + TOP_PAIRS_USDT) else: valid_pairs = set(TOP_PAIRS) self.enabled_pairs = [p for p in self.enabled_pairs if p in valid_pairs] self.blocked_pairs = [p for p in self.blocked_pairs if p in valid_pairs] # [SET] After filtering - enabled_pairs: {len(self.enabled_pairs)}, blocked_pairs: {len(self.blocked_pairs)}") # Initialize Binance client if API keys exist if self.api_key and self.api_secret: self.binance = BinanceClient(self.api_key, self.api_secret, self.paper_mode) # [SET] AFTER load_settings: max_simultaneous={self.max_simultaneous}, quote_currency={self.quote_currency}") # [SET] enabled_pairs count={len(self.enabled_pairs)}, sample={self.enabled_pairs[:5]}") # [SET] Robot Scanner Mode: {self.robot_scanner_mode}, Paper Mode: {self.paper_mode}") # [SET] Leverage: {self.leverage}X, Max Position: {self.max_position_pct*100}%") # Load trading modes self.falling_knife_enabled = settings.get('falling_knife_enabled', False) self.london_fade_enabled = settings.get('london_fade_enabled', False) if TRADING_MODES_AVAILABLE: if self.falling_knife_enabled and self.falling_knife: self.falling_knife.enable() if self.london_fade_enabled and self.london_fade: self.london_fade.enable() # Load unified filter mode self.filter_mode = settings.get('filter_mode', 'MIN_B') # Load Market Brain settings self.market_brain_enabled = settings.get('market_brain_enabled', True) self.market_brain_strict = settings.get('market_brain_strict', False) print(f"[SETTINGS] Market Brain: enabled={self.market_brain_enabled}, strict={self.market_brain_strict}") # Load multi-timeframe consensus settings self.lower_tf_priority = settings.get('lower_tf_priority', True) # Default: ON self.min_consensus = settings.get('min_consensus', 4) print(f"[SETTINGS] Multi-TF: lower_tf_priority={self.lower_tf_priority}, min_consensus={self.min_consensus}") # Load validation and scalping settings self.pre_trade_validation = settings.get('pre_trade_validation', False) # Default: OFF self.scalping_mode_enabled = settings.get('scalping_mode_enabled', False) # Default: OFF print(f"[SETTINGS] Validation: pre_trade_validation={self.pre_trade_validation}, scalping={self.scalping_mode_enabled}") # Sync min_grade_filter and min_trade_grade with filter_mode grade_map = { "OFF": "F", "A_ONLY": "A", "MIN_B": "B", "MIN_C": "C" } min_grade = grade_map.get(self.filter_mode, "B") self.min_grade_filter = min_grade self.min_trade_grade = min_grade print(f"[SETTINGS] Loaded filter_mode: {self.filter_mode} (Grade ≥ {min_grade})") self._update_settings_ui() def _update_settings_ui(self): try: if hasattr(self, 'asset_input'): self.asset_input.text = str(self.total_asset) print(f"[Settings] Updated asset_input: {self.total_asset}") if hasattr(self, 'risk_input'): self.risk_input.text = str(self.risk_per_trade * 100) print(f"[Settings] Updated risk_input: {self.risk_per_trade * 100}") if hasattr(self, 'max_trades_input'): self.max_trades_input.text = str(self.max_simultaneous) print(f"[Settings] Updated max_trades_input: {self.max_simultaneous}") if hasattr(self, 'max_trades_cycle_input'): self.max_trades_cycle_input.text = str(self.max_trades_per_cycle) print(f"[Settings] Updated max_trades_cycle_input: {self.max_trades_per_cycle}") if hasattr(self, 'api_key_input'): self.api_key_input.text = self.api_key print(f"[Settings] Updated api_key_input: {'*' * len(self.api_key) if self.api_key else 'empty'}") if hasattr(self, 'api_secret_input'): self.api_secret_input.text = self.api_secret print(f"[Settings] Updated api_secret_input: {'*' * len(self.api_secret) if self.api_secret else 'empty'}") # Update Robot Scanner UI elements if hasattr(self, 'leverage_input'): self.leverage_input.text = str(self.leverage) print(f"[Settings] Updated leverage_input: {self.leverage}") if hasattr(self, 'max_position_pct_input'): self.max_position_pct_input.text = str(self.max_position_pct * 100) print(f"[Settings] Updated max_position_pct_input: {self.max_position_pct * 100}") if hasattr(self, 'target_rr_input'): self.target_rr_input.text = str(self.target_rr_ratio) print(f"[Settings] Updated target_rr_input: {self.target_rr_ratio}") if hasattr(self, 'compound_toggle'): self.compound_toggle.state = 'down' if self.compound_trading else 'normal' self.compound_toggle.text = "[b]ON[/b]" if self.compound_trading else "[b]OFF[/b]" self.compound_toggle.background_color = GREEN if self.compound_trading else GRAY print(f"[Settings] Updated compound_toggle: {self.compound_trading}") if hasattr(self, 'smart_entry_toggle'): self.smart_entry_toggle.state = 'down' if self.smart_entry_enabled else 'normal' self.smart_entry_toggle.text = "[b]ON[/b]" if self.smart_entry_enabled else "[b]OFF[/b]" self.smart_entry_toggle.background_color = GREEN if self.smart_entry_enabled else GRAY print(f"[Settings] Updated smart_entry_toggle: {self.smart_entry_enabled}") # Update TP1/TP2 split UI if hasattr(self, 'tp1_pct_input'): self.tp1_pct_input.text = str(int(self.tp1_pct * 100)) print(f"[Settings] Updated tp1_pct_input: {self.tp1_pct * 100}") if hasattr(self, 'tp2_pct_label'): self.tp2_pct_label.text = f"[b]{int(self.tp2_pct * 100)}%[/b]" print(f"[Settings] Updated tp2_pct_label: {self.tp2_pct * 100}") if hasattr(self, 'trail_method_btn'): self.trail_method_btn.text = f"[b]{self.trail_method.upper()}[/b]" self.trail_method_btn.bg_color = AMBER if self.trail_method == 'percent' else CYAN print(f"[Settings] Updated trail_method_btn: {self.trail_method}") # Update notional value display in settings if hasattr(self, 'notional_settings_lbl'): notional = self._calculate_notional_value() self.notional_settings_lbl.text = f"[b]${notional:,.0f}[/b]" print(f"[Settings] Updated notional_settings_lbl: ${notional:,.0f}") if hasattr(self, 'quote_currency_btn'): self.quote_currency_btn.text = f"[b]{self.quote_currency}[/b]" if self.quote_currency == "USDC": self.quote_currency_btn.bg_color = CYAN elif self.quote_currency == "USDT": self.quote_currency_btn.bg_color = AMBER else: self.quote_currency_btn.bg_color = (0.6, 0.2, 0.8, 1) print(f"[Settings] Updated quote_currency: {self.quote_currency}") if hasattr(self, 'scan_mode_toggle'): scan_mode = getattr(self, 'scan_mode', 'fast') self.scan_mode_toggle.text = "[b]THOROUGH[/b]" if scan_mode == 'thorough' else "[b]FAST[/b]" self.scan_mode_toggle.bg_color = AMBER if scan_mode == 'thorough' else CYAN print(f"[Settings] Updated scan_mode: {scan_mode}") if hasattr(self, 'sound_theme_btn') and hasattr(self, 'sound_manager'): theme_names = {'pro_trader': 'PRO TRADER', 'cartoon': 'CARTOON', 'vegas': 'VEGAS'} theme_colors = {'pro_trader': CYAN, 'cartoon': PURPLE, 'vegas': GOLD} current_theme = self.sound_manager.current_theme self.sound_theme_btn.text = f"[b]{theme_names.get(current_theme, 'PRO TRADER')}[/b]" self.sound_theme_btn.bg_color = theme_colors.get(current_theme, CYAN) if hasattr(self, 'sound_enabled_btn') and hasattr(self, 'sound_manager'): enabled = self.sound_manager.enabled self.sound_enabled_btn.text = "[b]ON[/b]" if enabled else "[b]OFF[/b]" self.sound_enabled_btn.bg_color = GREEN if enabled else GRAY if hasattr(self, 'vibration_btn') and hasattr(self, 'sound_manager'): enabled = self.sound_manager.vibration_enabled self.vibration_btn.text = "[b]ON[/b]" if enabled else "[b]OFF[/b]" self.vibration_btn.bg_color = GREEN if enabled else GRAY print(f"[Settings] Updated vibration_btn: {enabled}") # Update TEST tab control buttons if hasattr(self, 'validation_btn'): validation_enabled = getattr(self, 'pre_trade_validation', False) self.validation_btn.text = "[b]ON[/b]" if validation_enabled else "[b]OFF[/b]" self.validation_btn.bg_color = GREEN if validation_enabled else GRAY print(f"[Settings] Updated validation_btn: {validation_enabled}") if hasattr(self, 'lower_tf_btn'): lower_tf_enabled = getattr(self, 'lower_tf_priority', True) self.lower_tf_btn.text = "[b]ON[/b]" if lower_tf_enabled else "[b]OFF[/b]" self.lower_tf_btn.bg_color = GREEN if lower_tf_enabled else DARK_GRAY print(f"[Settings] Updated lower_tf_btn: {lower_tf_enabled}") if hasattr(self, 'consensus_btn'): consensus_val = getattr(self, 'min_consensus', 4) self.consensus_btn.text = f"[b]{consensus_val} of 6[/b]" self.consensus_btn.bg_color = GREEN if consensus_val >= 5 else CYAN print(f"[Settings] Updated consensus_btn: {consensus_val}") if hasattr(self, 'grade_filter_btn'): grade_val = getattr(self, 'min_grade_filter', 'F') self.grade_filter_btn.text = f"[b]{grade_val}[/b]" print(f"[Settings] Updated grade_filter_btn: {grade_val}") self.update_pairs_count_display() except Exception as e: print(f"[Settings] Error updating UI: {e}") # ============================================ # ROBOT SCANNER MODE - TRADE SIZE CALCULATION # ============================================ def _calculate_trade_sizes(self): """ Calculate trade sizes based on available assets and max_position_pct. In paper mode: uses paper_usdc_allocation and paper_usdt_allocation In live mode: uses usdc_balance and usdt_balance """ if self.paper_mode: # Paper mode: use configured allocations self.usdc_trade_size = min( self.paper_usdc_allocation * self.max_position_pct, self.paper_usdc_allocation / self.max_simultaneous if self.max_simultaneous > 0 else self.paper_usdc_allocation ) self.usdt_trade_size = min( self.paper_usdt_allocation * self.max_position_pct, self.paper_usdt_allocation / self.max_simultaneous if self.max_simultaneous > 0 else self.paper_usdt_allocation ) else: # Live mode: use actual balances if self.usdc_enabled and self.usdc_balance > 0: self.usdc_trade_size = min( self.usdc_balance * self.max_position_pct, self.usdc_balance / self.max_simultaneous if self.max_simultaneous > 0 else self.usdc_balance * self.max_position_pct ) else: self.usdc_trade_size = 0 if self.usdt_enabled and self.usdt_balance > 0: self.usdt_trade_size = min( self.usdt_balance * self.max_position_pct, self.usdt_balance / self.max_simultaneous if self.max_simultaneous > 0 else self.usdt_balance * self.max_position_pct ) else: self.usdt_trade_size = 0 print(f"[TRADE SIZE] USDC: ${self.usdc_trade_size:,.2f}, USDT: ${self.usdt_trade_size:,.2f}") def get_available_balance_for_pair(self, pair): """Get available balance for a specific pair based on its quote currency.""" if 'USDT' in pair: return self.usdt_trade_size if self.usdt_trade_size > 0 else self.total_asset * self.max_position_pct / 2 else: return self.usdc_trade_size if self.usdc_trade_size > 0 else self.total_asset * self.max_position_pct / 2 # ============================================ # SECURITY & API MANAGEMENT # ============================================ def _hash_password(self, password): """Hash a password for secure storage.""" import hashlib return hashlib.sha256(password.encode()).hexdigest() def verify_password(self, password): """Verify a password against stored hash.""" if not self.user_password_hash: return True # No password set return self._hash_password(password) == self.user_password_hash def set_user_password(self, password): """Set user password for app protection.""" self.user_password_hash = self._hash_password(password) self.save_security_settings() def save_security_settings(self): """Save security settings including encrypted API keys.""" settings = safe_json_load(SETTINGS_FILE, {}) # Security settings settings['user_password_hash'] = self.user_password_hash settings['user_email'] = self.user_email settings['user_phone'] = self.user_phone settings['google_auth_enabled'] = self.google_auth_enabled settings['google_auth_secret'] = self.google_auth_secret # API keys (in production, these should be encrypted) settings['api_key'] = self.api_key settings['api_secret'] = self.api_secret settings['api_key_encrypted'] = self.api_key_encrypted settings['live_trading_configured'] = self.live_trading_configured # Asset balances settings['usdc_balance'] = self.usdc_balance settings['usdt_balance'] = self.usdt_balance settings['usdc_enabled'] = self.usdc_enabled settings['usdt_enabled'] = self.usdt_enabled safe_json_save(SETTINGS_FILE, settings) print("[SECURITY] Security settings saved") def setup_live_trading(self, api_key, api_secret, usdc_amount=0, usdt_amount=0): """ Configure live trading with API keys and asset amounts. Args: api_key: Binance API key api_secret: Binance API secret usdc_amount: Amount of USDC available for trading usdt_amount: Amount of USDT available for trading """ self.api_key = api_key self.api_secret = api_secret self.usdc_balance = usdc_amount self.usdt_balance = usdt_amount self.usdc_enabled = usdc_amount > 0 self.usdt_enabled = usdt_amount > 0 self.live_trading_configured = True self.paper_mode = False # Update total asset self.total_asset = usdc_amount + usdt_amount # Recalculate trade sizes self._calculate_trade_sizes() # Initialize Binance client self.binance = BinanceClient(api_key, api_secret, paper=False) # Update pairs based on available assets if self.usdc_enabled and self.usdt_enabled: self.quote_currency = "BOTH" self.enabled_pairs = TOP_PAIRS.copy() + TOP_PAIRS_USDT.copy() elif self.usdc_enabled: self.quote_currency = "USDC" self.enabled_pairs = TOP_PAIRS.copy() elif self.usdt_enabled: self.quote_currency = "USDT" self.enabled_pairs = TOP_PAIRS_USDT.copy() # Save settings self.save_security_settings() self.save_all_settings() self.log_activity(f"[b]LIVE TRADING CONFIGURED[/b] - USDC: ${usdc_amount:,.2f}, USDT: ${usdt_amount:,.2f}", "SUCCESS") def save_all_settings(self): """Save all settings including Robot Scanner settings.""" settings = { # Robot Scanner mode 'robot_scanner_mode': self.robot_scanner_mode, 'bot_engaged': self.bot_engaged, # Paper mode settings 'paper_mode': self.paper_mode, 'paper_total_asset': self.paper_total_asset, 'paper_usdc_allocation': self.paper_usdc_allocation, 'paper_usdt_allocation': self.paper_usdt_allocation, # Trading parameters 'total_asset': self.total_asset, 'risk_per_trade': self.risk_per_trade, 'target_rr_ratio': self.target_rr_ratio, 'min_rr_ratio': self.min_rr_ratio, 'max_position_pct': self.max_position_pct, 'max_simultaneous': self.max_simultaneous, 'max_trades_per_cycle': self.max_trades_per_cycle, # Leverage 'leverage': self.leverage, 'max_leverage': self.max_leverage, # Smart entry and compound 'smart_entry_enabled': self.smart_entry_enabled, 'compound_trading': self.compound_trading, 'compound_pct': self.compound_pct, # TP/SL settings 'tp1_pct': self.tp1_pct, 'tp2_pct': self.tp2_pct, 'trail_pct': self.trail_pct, 'move_sl_to_tp1': self.move_sl_to_tp1, 'trail_start_pct': self.trail_start_pct, 'sl_buffer_after_tp1': self.sl_buffer_after_tp1, 'trail_method': self.trail_method, # Scanning settings 'auto_refresh': self.auto_refresh, 'continuous_scan': self.continuous_scan, 'auto_scan_interval': self.auto_scan_interval, 'trade_direction': self.trade_direction, 'scan_mode': self.scan_mode, # RESPECT OFF SWITCH - User disabled flags 'optimus_user_disabled': getattr(self, 'optimus_user_disabled', False), 'big_brain_user_disabled': getattr(self, 'big_brain_user_disabled', False), # Security (from security settings) 'user_password_hash': self.user_password_hash, 'user_email': self.user_email, 'user_phone': self.user_phone, 'google_auth_enabled': self.google_auth_enabled, 'api_key': self.api_key, 'api_secret': self.api_secret, 'api_key_encrypted': self.api_key_encrypted, 'live_trading_configured': self.live_trading_configured, # Assets 'usdc_balance': self.usdc_balance, 'usdt_balance': self.usdt_balance, 'usdc_enabled': self.usdc_enabled, 'usdt_enabled': self.usdt_enabled, # Circuit breaker 'circuit_breaker_enabled': self.circuit_breaker_enabled, 'circuit_breaker_max_daily_loss': self.circuit_breaker_max_daily_loss, # Sound settings 'sound_enabled': self.sound_manager.enabled if hasattr(self, 'sound_manager') else True, 'sound_theme': self.sound_manager.current_theme if hasattr(self, 'sound_manager') else 'pro_trader', 'vibration_enabled': self.sound_manager.vibration_enabled if hasattr(self, 'sound_manager') else True, # Pairs 'quote_currency': self.quote_currency, 'enabled_pairs': self.enabled_pairs, 'blocked_pairs': self.blocked_pairs, 'min_trade_grade': self.min_trade_grade, # Trading modes 'falling_knife_enabled': getattr(self, 'falling_knife_enabled', False), 'london_fade_enabled': getattr(self, 'london_fade_enabled', False), # Unified filter mode 'filter_mode': getattr(self, 'filter_mode', 'MIN_B'), # Market Brain settings 'market_brain_enabled': getattr(self, 'market_brain_enabled', True), 'market_brain_strict': getattr(self, 'market_brain_strict', False), # Multi-timeframe consensus settings 'lower_tf_priority': getattr(self, 'lower_tf_priority', False), 'min_consensus': getattr(self, 'min_consensus', 4), # Validation and scalping settings 'pre_trade_validation': getattr(self, 'pre_trade_validation', False), 'scalping_mode_enabled': getattr(self, 'scalping_mode_enabled', False), } safe_json_save(SETTINGS_FILE, settings) print("[SETTINGS] All settings saved successfully") def show_popup(self, title, message): from kivy.uix.scrollview import ScrollView content = BoxLayout(orientation='vertical', padding=(dp(25), dp(15), dp(15), dp(15)), spacing=dp(10)) scroll = ScrollView(size_hint_y=0.9) msg_label = Label( text=message, color=WHITE, font_size=sp(11), markup=True, size_hint_y=None, text_size=(Window.width * 0.70, None) ) msg_label.bind(texture_size=lambda lbl, size: setattr(lbl, 'height', size[1] + dp(20))) scroll.add_widget(msg_label) content.add_widget(scroll) close_btn = Button( text="CLOSE", background_color=GREEN, color=WHITE, font_size=sp(12), size_hint_y=0.1 ) popup = Popup(title=title, content=content, size_hint=(0.85, 0.7), background_color=CARD_BG) close_btn.bind(on_press=popup.dismiss) content.add_widget(close_btn) popup.open() if __name__ == '__main__': AlphaScannerApp().run()