ap_enabled = True self.orderbook_imbalance_enabled = True self.scalping_risk_pct = 0.5 self.scalping_target_rr = 0.8 self.scalping_max_hold_seconds = 300 # 5 min max hold self.scalping_profit_target = 0.4 # 0.4% profit target self.scalping_stop_loss = 0.5 # 0.5% stop loss self.last_orderbook_data = {} # Cache orderbook analysis # ============================================ # MONEY MAKER MODE v1.0 - GUARANTEED PROFIT ENGINE # ============================================ self.money_maker_mode = False self.money_maker_session_start = None self.money_maker_session_profit = 0.0 self.money_maker_target_preset = 10.0 # Default $10 target, adjustable to $5 self.money_maker_target_profit = 10.0 # Current target (copied from preset) self.money_maker_max_loss = 20.0 # 2x target (auto-adjusted) self.money_maker_session_duration = 21600 # 6 hours in seconds self.money_maker_compound_capital = 1500.0 # 30% of $5000 self.money_maker_base_leverage = 5 # 5x leverage self.money_maker_rr_ratio = 1.5 # 1:1.5 R/R self.money_maker_stop_pct = 0.15 # 0.15% stop (HALF of 0.3%) self.money_maker_target_pct = 0.225 # 0.225% target (HALF of 0.45%) self.money_maker_max_trades = 50 # Max trades per session self.money_maker_trade_count = 0 self.money_maker_strategies = ['vwap_reversion', 'momentum_burst', 'breakout_scalp'] self.money_maker_active_trades = [] # Track active MM trades # ============================================ # MONEY MAKER PERFORMANCE TRACKER v1.0 # ============================================ self.mm_tracker = { 'total_sessions': 0, 'profitable_sessions': 0, 'losing_sessions': 0, 'total_trades': 0, 'winning_trades': 0, 'losing_trades': 0, 'total_profit': 0.0, 'total_loss': 0.0, 'best_session': 0.0, 'worst_session': 0.0, 'avg_session_pnl': 0.0, 'win_rate': 0.0, 'avg_trade_pnl': 0.0, 'strategy_performance': { 'VWAP': {'trades': 0, 'wins': 0, 'profit': 0.0}, 'MOMENTUM': {'trades': 0, 'wins': 0, 'profit': 0.0}, 'BREAKOUT': {'trades': 0, 'wins': 0, 'profit': 0.0} } } self.mm_log_dir = "money_maker_logs" self._ensure_mm_log_dir() self._load_mm_tracker() # Load historical stats # self.setup_queue already initialized as SetupQueue above self.setup_validation_interval = 10 self.last_setup_validation = 0 self.pending_entry = None self.momentum_breakout_threshold = 2.0 self.dynamic_tp_enabled = True # Backtest logger stub self.backtest_logger = BacktestLogger() # Initialize 24/7 Market Brain self.market_brain = MarketBrainPro(max_positions=getattr(self, 'max_simultaneous', 3)) # Initialize OPTIMUS BRAIN self._init_optimus_brain() # Circuit breaker stub self.circuit_breaker = CircuitBreaker() # Trade logger for analytics self.trade_logger = TradeLogger(DATA_DIR) # Setup analytics logger for comprehensive tracking self.analytics_logger = SetupAnalyticsLogger(DATA_DIR) print("[INIT] SetupAnalyticsLogger initialized - all setups will be tracked") # Improvement logger for user feedback self.improvement_logger = ImprovementLogger(DATA_DIR) print("[INIT] ImprovementLogger initialized - user feedback tracking active") # Auto-clear blocked pairs on startup to allow trading if hasattr(self.improvement_logger, 'disqualified_pairs'): blocked_count = len(self.improvement_logger.disqualified_pairs) if blocked_count > 0: self.improvement_logger.disqualified_pairs = {} self.improvement_logger._save_disqualified() print(f"[STARTUP] Auto-cleared {blocked_count} blocked pairs") self.log_activity(f"[STARTUP] Auto-cleared {blocked_count} blocked pairs - trading resumed", "SUCCESS") # Connect trade_logger to position_manager self.position_manager.trade_logger = self.trade_logger # Initialize BOT-FATHER (intelligent trade monitor) self.bot_father = BotFather(self.position_manager, self.binance, self) print("[INIT] BOT-FATHER initialized - adaptive position monitoring active") # Initialize SetupScout (smart scanning with time-aware intervals) self.setup_scout = SetupScout(self.binance) print("[INIT] SetupScout initialized - time-aware scanning active") # Initialize Precision Indicator Engine self.indicator_monitor = IndicatorMonitor(self) self.divergence_scanner = DivergenceScanner(self.indicator_monitor) self.adx_filter = ADXFilter(self.indicator_monitor) self.liquidity_sweep_detector = LiquiditySweepDetector(self.indicator_monitor) print("[INIT] Precision Indicator Engine initialized") print("[INIT] - Divergence Scanner (RSI divergence detection)") print("[INIT] - ADX Filter (trend strength filtering)") print("[INIT] - Liquidity Sweep Detector (smart money patterns)") # Initialize TODO List Features Phase 1-4 self.volume_profile = VolumeProfile(self.indicator_monitor) self.cvd_monitor = CVDMonitor(self.indicator_monitor) self.vwap_bands = VWAPBands(self.indicator_monitor) self.pattern_recognizer = PatternRecognizer(self.indicator_monitor) self.market_scorer = MarketConditionScorer(self.indicator_monitor) self.correlation_monitor = CorrelationMonitor(self.binance, self.indicator_monitor) self.session_analyzer = SessionAnalyzer(self.indicator_monitor) self.heatmap_analyzer = HeatmapAnalyzer(self.indicator_monitor) self.slippage_monitor = SlippageMonitor(self.indicator_monitor) self.streak_adapter = StreakAdapter(self.position_manager, self.indicator_monitor) self.botfather_v2 = BotFatherV2(self.position_manager, self.binance, self.vwap_bands, self.volume_profile, self.indicator_monitor) # Initialize Entry Timing Engine self.entry_timing = EntryTimingAnalyzer(self.indicator_monitor) # Initialize Adaptive Entry Engine self.adaptive_entry = AdaptiveEntryEngine(self.binance, self.indicator_monitor) print("[INIT] Entry Timing Engine initialized") print("[INIT] - RSI momentum confirmation") print("[INIT] - StochRSI overbought/oversold timing") print("[INIT] - Orderbook depth analysis") print("[INIT] Adaptive Entry Engine initialized") print("[INIT] - Market Anchor Monitor (BTC/DXY/Crude)") print("[INIT] - Momentum vs Mean Reversion detection") print("[INIT] - Auto-scalp mode activation") print("[INIT] - Adaptive stop/target calculation") print("[INIT] TODO List Features initialized") print("[INIT] - Volume Profile (POC/VAH/VAL)") print("[INIT] - CVD Monitor (order flow)") print("[INIT] - VWAP Bands (mean reversion)") print("[INIT] - Pattern Recognition (flags/triangles)") print("[INIT] - Market Condition Scorer (0-100)") print("[INIT] - Correlation Monitor (risk management)") print("[INIT] - Session Analysis (Asian/London/NY)") print("[INIT] - Slippage Monitor (execution quality)") print("[INIT] - Streak Adapter (psychology)") print("[INIT] - BOT-FATHER 2.0 (heat monitoring)") # Trading modes initialization if TRADING_MODES_AVAILABLE: self.falling_knife = get_falling_knife_mode() self.london_fade = get_london_close_fade_mode() self.falling_knife_enabled = False self.london_fade_enabled = False else: self.falling_knife = None self.london_fade = None self.falling_knife_enabled = False self.london_fade_enabled = False # Smart trailing stop manager (uses order book analysis) if SMART_TRAIL_AVAILABLE: self.smart_trail_manager = SmartTrailingManager(self.binance) self.use_smart_trailing = True else: self.smart_trail_manager = None self.use_smart_trailing = False # Data logging for optimization self.data_logging_enabled = False self.live_data_log = [] # Stores live market data for analysis self.max_live_data_entries = 10000 self.data_log_interval = 60 # Log every 60 seconds self.last_data_log = 0 self.entry_distance_history = {} self.entry_widening_threshold = 0.5 self.entry_monitor_window = 300 self.setup_history = [] self.max_setup_history = 200 self.asset_log = [] self.max_asset_log = 500 self.asset_log_interval = 300 self.last_asset_log = 0 self.mode_buttons = {} self.order_type_buttons = {} self.activity_logs = [] self._thread_pool = ThreadPoolExecutor(max_workers=3, thread_name_prefix="bot_worker") self._pending_futures = [] self._api_last_call = 0 self._api_min_interval = 0.05 self._api_call_count = 0 self._api_count_reset = time.time() self._perf_stats = {'api_calls': 0, 'threads_spawned': 0, 'ui_updates': 0} # Performance tracking for TEST tab self._perf_scan_time = 0 self._perf_pairs_scanned = 0 self._perf_setups_found = 0 # SETUP RESULT CACHE: Avoid re-scanning same pair within TTL self._setup_cache = {} # {pair: {'setup': setup, 'timestamp': time, 'tf_data': {...}}} self._setup_cache_ttl = 15 # seconds - 1h TF doesn't change much faster # ORDERBOOK CACHE: Cache orderbook for spoof detection (expensive to fetch) self._orderbook_cache = {} # {pair: {'data': orderbook, 'timestamp': time}} self._orderbook_cache_ttl = 15 # seconds - increased to reduce API calls # KLINES CACHE: Cache klines data to avoid repeated API calls self._klines_cache = {} # {pair: {timeframe: {'data': klines, 'timestamp': time}}} self._klines_cache_ttl = 30 # seconds - klines update every minute anyway # PRICE CACHE: Cache ticker prices self._price_cache = {} # {pair: {'price': price, 'timestamp': time}} self._price_cache_ttl = 3 # seconds - price changes constantly # P&L tracking (initialized here, loaded from file in load_settings) self.daily_pnl = 0 self.weekly_pnl = 0 self.total_pnl = 0 # Scan completion tracking - bot waits for full scan before trading self._scan_completed = False self._scan_stop_requested = False # Flag to manually stop scanning self._last_scan_time = 0 self._widget_refs = [] self.load_settings() # Only set defaults if not loaded from settings # DEFAULT: Show all grades (F), user can set stricter filter in UI if not hasattr(self, 'min_grade_filter') or self.min_grade_filter is None: self.min_grade_filter = "F" if not hasattr(self, 'min_trade_grade') or self.min_trade_grade is None: self.min_trade_grade = "F" # NOTE: max_simultaneous is loaded from settings - don't override # self.max_simultaneous = 5 # REMOVED - was causing grade filter to be ignored print(f"[BUILD DEBUG] AFTER load_settings, max_simultaneous={self.max_simultaneous}") # Bot engagement state is already set in load_settings() from 'robot_scanner_mode' # Do NOT override it here - respect user's setting # self.bot_engaged = True # REMOVED - was overwriting user setting # Only set defaults if not loaded from settings if not hasattr(self, 'time_restrictions_enabled'): self.time_restrictions_enabled = False if not hasattr(self, 'trading_hours_mode') or self.trading_hours_mode is None: self.trading_hours_mode = "24/7" # FIX: Don't disable all limits - respect user's grade filter setting # self.all_limits_off = True # REMOVED - was causing grade filter to be ignored # Update UI to reflect default settings (UI will be built after this) # Button state will be synced after UI is built self.position_manager.risk_per_trade = self.risk_per_trade # Sync P&L from trade_logger if available if hasattr(self, 'trade_logger'): self.daily_pnl = getattr(self.trade_logger, 'daily_pnl', self.daily_pnl) self.weekly_pnl = getattr(self.trade_logger, 'weekly_pnl', self.weekly_pnl) self.total_pnl = getattr(self.trade_logger, 'total_pnl', self.total_pnl) root = BoxLayout(orientation='vertical', padding=dp(4), spacing=dp(4)) header = BoxLayout(size_hint_y=0.08, spacing=dp(6)) # Header with robot icon header_icon_box = BoxLayout(orientation='horizontal', size_hint_x=0.28, padding=dp(2)) robot_img = get_icon_image('BOT', size=dp(18)) if robot_img: robot_img.pos_hint = {'center_y': 0.5} header_icon_box.add_widget(robot_img) title_lbl = EmojiLabel(text="[b]ROB-BOT[/b]", markup=True, color=GOLD, font_size=sp(12)) title_lbl.pos_hint = {'center_y': 0.5} header_icon_box.add_widget(title_lbl) header.add_widget(header_icon_box) self.time_lbl = Label(text="--:--", markup=True, color=WHITE, font_size=sp(10), size_hint_x=0.14) self.bot_status = EmojiLabel(text="[b][-] STBY[/b]", markup=True, color=GRAY, font_size=sp(9), size_hint_x=0.20) self.analyzing_lbl = Label(text="", markup=True, color=AMBER, font_size=sp(8), size_hint_x=0.38) # SWAPPED: bot_status (ENGAGED) now comes before time_lbl (clock) header.add_widget(self.bot_status) header.add_widget(self.time_lbl) header.add_widget(self.analyzing_lbl) root.add_widget(header) tab_bar = BoxLayout(size_hint_y=0.06, spacing=dp(4), padding=dp(2)) self.tabs = {} tab_icon_names = { 'SETUPS': 'CHART', 'ROB-BOT': 'BOT', 'POSITIONS': 'BAG', 'ASSETS': 'PNL', 'TEST': 'TEST', 'SETTINGS': 'SETTINGS', 'LOGS': 'LOG' } for name in ['SETUPS', 'ROB-BOT', 'POSITIONS', 'ASSETS', 'TEST', 'SETTINGS', 'LOGS']: # Create custom tab button with icon icon_name = tab_icon_names.get(name, '') tab_btn = self._create_tab_button(name, icon_name, name == 'SETUPS') tab_bar.add_widget(tab_btn) self.tabs[name] = tab_btn root.add_widget(tab_bar) self.tab_content = BoxLayout(orientation='vertical') root.add_widget(self.tab_content) screens_to_build = [ ('setups', self.build_setups_screen), ('rob_bot', self.build_rob_bot_screen), ('positions', self.build_positions_screen), ('assets', self.build_assets_screen), ('test', self.build_test_screen), ('settings', self.build_settings_screen), ('logs', self.build_logs_screen), ] for name, builder in screens_to_build: try: builder() except Exception as e: print(f"[ERROR] build_{name}_screen failed: {e}") import traceback traceback.print_exc() # Load Falling Knife settings if hasattr(self, 'falling_knife') and self.falling_knife: fk_enabled = getattr(self, 'falling_knife_enabled', False) if fk_enabled: self.falling_knife.enable() if hasattr(self, 'fk_toggle'): self.fk_toggle.state = "down" self.fk_toggle.text = "ON" self.fk_toggle.background_color = GREEN if hasattr(self, 'fk_status_label'): self.fk_status_label.text = "Status: [color=00ff00]Armed (Auto)[/color]" self._update_settings_ui() # Sync engage button state with loaded setting self._sync_engage_button_state() self.switch_tab('ROB-BOT') Clock.schedule_interval(self.update_time, 1) Clock.schedule_interval(self.bot_loop, 10) Clock.schedule_interval(self.update_positions_display, 15) # Reduced from 10s to 15s Clock.schedule_interval(self.update_market_data, 60) # Reduced from 30s to 60s Clock.schedule_interval(self.auto_refresh_setups, 30) # Keep 30s for adaptive scanning Clock.schedule_interval(self.update_funding_rates, 3600) Clock.schedule_interval(self._cleanup_widgets, 60) Clock.schedule_interval(self._cleanup_caches, 300) # Clean caches every 5 minutes Clock.schedule_interval(self._log_bot_status, 300) # Log status every 5 minutes Clock.schedule_interval(self._log_perf_stats, 60) self.funding_data = {'longs': [], 'shorts': [], 'last_update': None} self.funding_countdown = 3600 Clock.schedule_once(lambda dt: self._do_initial_scan(), 2) return root def _do_initial_scan(self): """Do initial market scan and auto-start Optimus Brain in aggressive mode.""" if not self.initial_scan_done: self.initial_scan_done = True self.last_auto_scan = time.time() self.log_activity("[b]INITIAL SCAN:[/b] Starting market scan on startup...") self.force_market_scan() # Auto-start Optimus Brain with aggressive preset after UI is built Clock.schedule_once(lambda dt: self._auto_start_optimus(), 5) def _auto_start_optimus(self): """Auto-start Optimus Brain and Big Brain at startup.""" try: print("[OPTIMUS AUTO-START] Starting...") # RESPECT OFF SWITCH: Skip if user disabled Optimus if getattr(self, 'optimus_user_disabled', False): print("[OPTIMUS AUTO-START] BLOCKED - User turned Optimus OFF") self.log_activity("[OPTIMUS] Auto-start blocked - user disabled", "INFO") return # Ensure Optimus Brain is initialized first if not hasattr(self, '_optimus_config'): self._init_optimus_brain() print("[OPTIMUS AUTO-START] Brain initialized") # RESPECT OFF SWITCH: Skip Big Brain if user disabled if getattr(self, 'big_brain_user_disabled', False): print("[BIG BRAIN AUTO-START] BLOCKED - User turned Big Brain OFF") self.log_activity("[BIG BRAIN] Auto-start blocked - user disabled", "INFO") else: # Enable Big Brain self.big_brain_enabled = True self.log_activity("[b][BIG BRAIN][/b] Auto-enabled on startup", "SUCCESS") print("[OPTIMUS AUTO-START] Big Brain enabled") # Set aggressive preset self._set_optimus_preset('aggressive') print("[OPTIMUS AUTO-START] Aggressive preset set") # CRITICAL: Enable Optimus override so it shows as ON in modes menu self.optimus_override = True print(f"[OPTIMUS AUTO-START] Override set to: {self.optimus_override}") # Update UI if button exists if hasattr(self, 'optimus_override_btn'): self.optimus_override_btn.text = "[b]OPTIMUS: ACTIVE[/b]" self.optimus_override_btn.set_bg_color((0.2, 0.8, 0.2, 1)) print("[OPTIMUS AUTO-START] Button UI updated") else: print("[OPTIMUS AUTO-START] Button not yet created (will show ON in menu)") # Start the optimization loop self.start_autonomous_optimization() self.log_activity("[b][OPTIMUS][/b] Auto-started in AGGRESSIVE mode", "SUCCESS") self.log_activity("[b][OPTIMUS][/b] Override enabled - Optimus controls trading", "SUCCESS") print("[OPTIMUS AUTO-START] COMPLETE - Override is ON") except Exception as e: print(f"[OPTIMUS AUTO-START] FAILED: {e}") import traceback print(traceback.format_exc()) def _create_tab_button(self, name, icon_name, is_active=False): """Create a tab button with icon image and text - icon inside button.""" from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.graphics import Color, RoundedRectangle # Get colors bg_color = GOLD if is_active else CARD_BG text_color = BLACK if is_active else WHITE # Create button layout with icon+text inside btn = Button( markup=True, background_color=(0, 0, 0, 0), color=text_color, font_size=sp(8), size_hint=(1, 1), halign='center' ) btn._tab_name = name btn._bg_color = bg_color btn._icon_name = icon_name # Build text with icon placeholder - we'll use a simpler approach # Just text for now, icons can be added via button background or text btn.text = f"[b]{name}[/b]" # Store reference for color switching def update_bg(*args): btn.canvas.before.clear() with btn.canvas.before: Color(*btn._bg_color) RoundedRectangle(pos=btn.pos, size=btn.size, radius=[dp(12)]*4) btn.bind(pos=update_bg, size=update_bg) Clock.schedule_once(update_bg, 0) # Bind click def on_press(*args): self.switch_tab(name) btn.bind(on_press=on_press) return btn def _sync_engage_button_state(self): """Sync engage button and status label with bot_engaged state""" if self.bot_engaged: # Bot is engaged if hasattr(self, 'engage_btn'): self.engage_btn.text = "[b]DISENGAGE[/b]" self.engage_btn.set_bg_color(DARK_RED) if hasattr(self, 'bot_status'): self.bot_status.text = "[b]ENGAGED[/b]" self.bot_status.color = GREEN self.log_activity("[b]BOT STATUS:[/b] Auto-engaged on startup (robot mode)") else: # Bot is disengaged if hasattr(self, 'engage_btn'): self.engage_btn.text = "[b]ENGAGE BOT[/b]" self.engage_btn.set_bg_color(DARK_GREEN) if hasattr(self, 'bot_status'): self.bot_status.text = "[b]STANDBY[/b]" self.bot_status.color = GRAY self.log_activity("[b]BOT STATUS:[/b] Standby on startup") def get_min_score(self): """Get minimum score threshold based on filter mode""" grade_to_score = { 'A+': 95, 'A': 90, 'A-': 85, 'B+': 80, 'B': 75, 'B-': 70, 'C+': 65, 'C': 60, 'C-': 55, 'D+': 50, 'D': 45, 'F': 30 } # Use min_grade_filter if available if hasattr(self, 'min_grade_filter'): return grade_to_score.get(self.min_grade_filter, 75) # Fallback to filter_mode if hasattr(self, 'filter_mode'): filter_scores = { 'OFF': 30, # All grades 'A_ONLY': 90, # A grades only 'MIN_B': 75, # B and above 'MIN_C': 60 # C and above } return filter_scores.get(self.filter_mode, 75) # Default fallback return 75 def _cleanup_widgets(self, dt=None): try: self._widget_refs = [ref for ref in self._widget_refs if ref.parent is not None] self._pending_futures = [f for f in self._pending_futures if not f.done()] if len(self.activity_logs) > 500: self.activity_logs = self.activity_logs[-250:] if len(self.setup_history) > self.max_setup_history: self.setup_history = self.setup_history[-self.max_setup_history//2:] except Exception as e: print(f"[CLEANUP] Error: {e}") def _cleanup_caches(self, dt=None): """Clean expired cache entries to prevent memory leaks.""" try: now = time.time() # Clean orderbook cache expired = [k for k, v in self._orderbook_cache.items() if (now - v['timestamp']) > self._orderbook_cache_ttl * 2] for k in expired: del self._orderbook_cache[k] # Clean klines cache for pair in list(self._klines_cache.keys()): expired_tf = [tf for tf, v in self._klines_cache[pair].items() if (now - v['timestamp']) > self._klines_cache_ttl * 2] for tf in expired_tf: del self._klines_cache[pair][tf] # Remove pair if no timeframes left if not self._klines_cache[pair]: del self._klines_cache[pair] # Clean price cache expired = [k for k, v in self._price_cache.items() if (now - v['timestamp']) > self._price_cache_ttl * 2] for k in expired: del self._price_cache[k] # Clean setup cache expired = [k for k, v in self._setup_cache.items() if (now - v['timestamp']) > self._setup_cache_ttl * 2] for k in expired: del self._setup_cache[k] if expired: print(f"[CACHE CLEANUP] Removed {len(expired)} expired entries") except Exception as e: print(f"[CACHE CLEANUP] Error: {e}") def _check_lag(self, operation_name=""): """Check if operation is taking too long and throttle if needed.""" if not getattr(self, '_lag_prevention_enabled', False): return False now = time.time() if hasattr(self, '_last_frame_time'): frame_time = now - self._last_frame_time if frame_time > self._lag_detection_threshold: print(f"[LAG DETECTED] {operation_name} took {frame_time*1000:.0f}ms - throttling") return True self._last_frame_time = now return False def _should_skip_heavy_operation(self): """Return True if we should skip this heavy operation to prevent lag.""" self._frame_skip_counter = getattr(self, '_frame_skip_counter', 0) + 1 if self._frame_skip_counter >= self._heavy_operation_throttle: self._frame_skip_counter = 0 return False # Don't skip return True # Skip this one def _batch_widget_update(self, container, widgets, batch_size=None): """Update widgets in batches to prevent UI freezing.""" if batch_size is None: batch_size = getattr(self, '_ui_update_batch_size', 5) total = len(widgets) for i in range(0, total, batch_size): batch = widgets[i:i+batch_size] for widget in batch: container.add_widget(widget) # Yield to UI thread every batch if i + batch_size < total: import time time.sleep(0.001) # 1ms yield def _log_perf_stats(self, dt=None): try: # Check for lag lag_detected = self._check_lag("perf_stats") perf_msg = f"[PERF] API: {self._perf_stats.get('api_calls', 0)}, Threads: {self._perf_stats.get('threads_spawned', 0)}, UI: {self._perf_stats.get('ui_updates', 0)}" if lag_detected: perf_msg += " [LAG DETECTED - THROTTLING]" print(perf_msg) self._perf_stats = {'api_calls': 0, 'threads_spawned': 0, 'ui_updates': 0} except: pass def _log_bot_status(self, dt=None): """Log BOT-FATHER and system status every 5 minutes""" try: # Get open positions open_pos = [p for p in self.position_manager.positions if p['status'] == 'OPEN'] # Get scan interval if hasattr(self, 'setup_scout'): scan_interval = self.setup_scout.get_scan_interval() scan_mins = scan_interval / 60 else: scan_mins = 1 # Log status status_msg = f"[BOT STATUS] Positions: {len(open_pos)}/2 | Scan interval: {scan_mins:.0f}min | BOT-FATHER: Active" print(status_msg) # Log to activity if available if hasattr(self, 'log_activity'): self.log_activity(f"[b]BOT STATUS:[/b] {len(open_pos)} positions | {scan_mins:.0f}min scan", "INFO") # Log behavior logger metrics if available try: from __main__ import get_behavior_logger logger = get_behavior_logger() if logger: summary = logger.get_session_summary() metrics = summary['metrics'] print(f"[BOT METRICS] Scans: {metrics['scans_performed']} | Setups: {metrics['setups_found']} | Trades: {metrics['trades_executed']}") except: pass # Log indicator metrics if available if hasattr(self, 'indicator_monitor') and self.indicator_monitor: try: summary = self.indicator_monitor.get_summary() print("[INDICATOR METRICS] Performance:") for indicator, accuracy in summary['accuracy'].items(): if summary['metrics'][indicator].get('calls', 0) > 0: calls = summary['metrics'][indicator]['calls'] print(f" {indicator:20s}: {accuracy:5.1f}% accuracy ({calls} calls)") except Exception as e: print(f"[INDICATOR METRICS ERROR] {e}") except Exception as e: print(f"[BOT STATUS ERROR] {e}") def on_stop(self): try: print("[APP] Shutting down, cleaning up threads...") self._thread_pool.shutdown(wait=False) except: pass # ============================================================================ # SETUP QUEUE SYSTEM - Producer/Consumer Methods # ============================================================================ def _on_new_setup(self, setup, is_update): """Callback when new setup is added to queue - trigger trade immediately.""" pair = setup.get('pair', 'Unknown') action = "UPDATED" if is_update else "NEW" print(f"[SETUP QUEUE] {action}: {pair} {setup.get('direction')} Grade {setup.get('grade')}") # Trigger trade check immediately (don't wait) if not is_update: # Only for new setups print(f"[SETUP QUEUE] Triggering immediate trade check for {pair}") from kivy.clock import Clock Clock.schedule_once(lambda dt: self.check_and_enter_trade(), 0) def _queue_maintenance(self, dt): """Periodic maintenance - cleanup expired, refresh stale setups.""" # Cleanup expired setups removed = self.setup_queue.cleanup_expired() if removed > 0: print(f"[QUEUE MAINTENANCE] Removed {removed} expired setups") # Refresh orderbook for setups older than refresh interval now = time.time() if now - self._last_queue_refresh > self._queue_refresh_interval: self._last_queue_refresh = now Clock.schedule_once(lambda dt: self._refresh_all_orderbooks(), 0) # Log queue status periodically status = self.setup_queue.get_queue_status() if status['total'] > 0: print(f"[QUEUE STATUS] {status['total']} setups (Fresh: {status['by_age']['fresh']}, Recent: {status['by_age']['recent']}, Aging: {status['by_age']['aging']}, Stale: {status['by_age']['stale']})") return True # Keep scheduled def _refresh_setup_orderbook(self, pair): """Refresh orderbook for a specific setup.""" try: # Use cached orderbook if available orderbook = self._get_cached_orderbook(pair) if orderbook: setup = self.setup_queue.get_setup(pair) if setup: setup['orderbook'] = orderbook setup['timestamp_updated'] = time.time() self.setup_queue.refresh_setup(pair, setup) print(f"[QUEUE REFRESH] Orderbook updated for {pair}") except Exception as e: print(f"[QUEUE REFRESH] Error refreshing {pair}: {e}") def _refresh_all_orderbooks(self): """Refresh orderbooks for top setups in queue (limited to reduce API load).""" setups = self.setup_queue.get_all_setups() # Only refresh top 5 setups to reduce API load and lag for setup in setups[:5]: pair = setup.get('pair') if pair: # Use thread pool for parallel refresh self._thread_pool.submit(self._refresh_setup_orderbook, pair) def get_setup_from_queue(self, pair=None, min_grade=None, direction=None, exclude_pairs=None): """Get a setup from the queue - used by trading logic.""" # Auto-cleanup before fetching self.setup_queue.cleanup_expired(max_age_override=180) # Stricter for trading if pair: return self.setup_queue.get_setup(pair, min_grade=min_grade, direction=direction) else: return self.setup_queue.get_setup(min_grade=min_grade, direction=direction, exclude_pairs=exclude_pairs) def get_all_setups_from_queue(self, min_grade=None, direction=None, exclude_pairs=None): """Get all matching setups from queue.""" self.setup_queue.cleanup_expired(max_age_override=180) return self.setup_queue.get_all_setups(min_grade=min_grade, direction=direction, exclude_pairs=exclude_pairs) def request_more_setups(self, pairs_needed): """Request scanner to prioritize specific pairs.""" print(f"[QUEUE REQUEST] Requesting scans for: {pairs_needed}") # Trigger immediate scan for these pairs Clock.schedule_once(lambda dt: self.force_scan_specific_pairs(pairs_needed), 0) # ============================================================================ # PENDING TRADE SYSTEM - Smart Selection + Active Monitoring # ============================================================================ def _select_pending_trade(self, setup, tier): """Select a setup as the pending trade.""" self.pending_trade = { 'setup': setup, 'pair': setup.get('pair'), 'direction': setup.get('direction', 'LONG'), 'grade': setup.get('grade', 'F'), 'score': setup.get('score', 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), 'selected_time': time.time(), 'tier': tier, 'recalc_count': 0, 'status': 'MONITORING' # MONITORING, READY, EXECUTED, RELEASED } print(f"[PENDING TRADE] Selected {setup.get('pair')} Grade {setup.get('grade')} from Tier {tier}") def _release_pending_trade(self): """Release the current pending trade.""" if self.pending_trade: print(f"[PENDING TRADE] Releasing {self.pending_trade.get('pair')}") self.pending_trade = None def _start_pending_trade_monitor(self): """Start monitoring the pending trade for entry execution.""" if self.pending_trade_monitor: Clock.unschedule(self.pending_trade_monitor) # Check every 2 seconds self.pending_trade_monitor = Clock.schedule_interval(self._monitor_pending_trade, 2) print(f"[PENDING TRADE] Started monitoring {self.pending_trade.get('pair')}") def _monitor_pending_trade(self, dt): """Monitor pending trade: fine-tune entry, check criteria, execute or release.""" if not self.pending_trade: return False # Stop monitoring trade = self.pending_trade pair = trade.get('pair') # Check if trade too old age = time.time() - trade.get('selected_time', 0) if age > self.pending_trade_max_age: self.log_activity(f"[PENDING] {pair} expired after {age:.0f}s - releasing", "WARN") self._release_pending_trade() return False # Check if position already open for this pair open_pos = [p for p in self.position_manager.positions if p.get('status') == 'OPEN' and p.get('pair') == pair] if open_pos: self.log_activity(f"[PENDING] {pair} already in position - releasing", "INFO") self._release_pending_trade() return False # Check if slots full open_count = len([p for p in self.position_manager.positions if p.get('status') == 'OPEN']) if open_count >= self.max_simultaneous: self.log_activity(f"[PENDING] Positions full ({open_count}/{self.max_simultaneous}) - holding", "INFO") return True # Keep monitoring # Get current price current_price = self._get_cached_price(pair) if not current_price: return True # Keep monitoring # Recalculate entry/SL/TP based on current conditions self._recalc_pending_trade(trade, current_price) # Check if entry criteria met entry = trade.get('entry', 0) direction = trade.get('direction') grade = trade.get('grade') # Entry criteria based on grade grade_priority = {'S': 10, 'A+': 9, 'A': 8, 'A-': 7, 'B+': 6, 'B': 5, 'B-': 4} priority = grade_priority.get(grade, 0) # For A grade or better: execute immediately if price near entry # For B grades: wait for better entry entry_tolerance = 0.001 if priority >= 7 else 0.002 # 0.1% for A+, 0.2% for B if direction == 'LONG': price_near_entry = current_price <= entry * (1 + entry_tolerance) price_good = current_price <= entry # At or below entry else: # SHORT price_near_entry = current_price >= entry * (1 - entry_tolerance) price_good = current_price >= entry # At or above entry # Execute if price near entry and grade is good if price_near_entry and priority >= 6: self.log_activity(f"[PENDING] {pair} entry criteria met! Price: {current_price:.4f}, Entry: {entry:.4f}", "TRADE") self._execute_pending_trade() return False # Stop monitoring # Log monitoring status every 10 seconds if int(time.time()) % 10 == 0: self.log_activity(f"[PENDING] {pair} | Price: {current_price:.4f} | Entry: {entry:.4f} | Grade: {grade}", "INFO") return True # Keep monitoring def _recalc_pending_trade(self, trade, current_price): """Recalculate entry, SL, TP based on current market conditions.""" pair = trade.get('pair') direction = trade.get('direction') try: # Fetch fresh data for recalculation klines = self.binance.get_klines(pair, '15m', 50) if not klines or len(klines) < 30: return prices = [float(k[4]) for k in klines] volumes = [float(k[5]) for k in klines] # Simple ATR for SL calculation atr = self._calculate_atr(prices, 14) # Recalculate entry based on current price action if direction == 'LONG': # Entry at support or below current if pullback new_entry = min(current_price * 0.998, prices[-5:][0] if len(prices) >= 5 else current_price) new_sl = new_entry - (atr * 1.5) new_tp1 = new_entry + (atr * 3) # 1:2 R/R new_tp2 = new_entry + (atr * 4.5) # 1:3 R/R else: # SHORT new_entry = max(current_price * 1.002, prices[-5:][0] if len(prices) >= 5 else current_price) new_sl = new_entry + (atr * 1.5) new_tp1 = new_entry - (atr * 3) new_tp2 = new_entry - (atr * 4.5) # Only update if significant change (>0.5%) old_entry = trade.get('entry', 0) if old_entry > 0 and abs(new_entry - old_entry) / old_entry > 0.005: trade['entry'] = new_entry trade['stop_loss'] = new_sl trade['take_profit1'] = new_tp1 trade['take_profit2'] = new_tp2 trade['recalc_count'] = trade.get('recalc_count', 0) + 1 print(f"[PENDING RECALC] {pair}: Entry {old_entry:.4f} -> {new_entry:.4f} (recalc #{trade['recalc_count']})") except Exception as e: print(f"[PENDING RECALC ERROR] {pair}: {e}") def _calculate_atr(self, prices, period=14): """Calculate Average True Range.""" if len(prices) < period + 1: return sum(abs(prices[i] - prices[i-1]) for i in range(1, len(prices))) / max(1, len(prices)-1) tr_values = [] for i in range(1, len(prices)): tr = abs(prices[i] - prices[i-1]) tr_values.append(tr) if len(tr_values) >= period: return sum(tr_values[-period:]) / period return sum(tr_values) / len(tr_values) if tr_values else prices[-1] * 0.01 def _get_cached_price(self, pair): """Get cached price for a pair.""" with self._market_data_lock: data = self.market_data.get(pair) if data and time.time() - data.get('timestamp', 0) < 30: return data.get('price') # Fetch fresh try: price = self.binance.get_ticker_price(pair) if price: with self._market_data_lock: self.market_data[pair] = {'price': price, 'timestamp': time.time()} return price except: return None def _execute_pending_trade(self): """Execute the pending trade.""" if not self.pending_trade: return trade = self.pending_trade # Build setup dict for execution setup = trade.get('setup', {}).copy() setup['entry'] = trade.get('entry') setup['stop_loss'] = trade.get('stop_loss') setup['take_profit1'] = trade.get('take_profit1') setup['take_profit2'] = trade.get('take_profit2') self.log_activity(f"[EXECUTING] {trade.get('pair')} {trade.get('grade')} setup", "TRADE") # Execute Clock.schedule_once(lambda dt: self._execute_trade(setup), 0) # Mark as executed trade['status'] = 'EXECUTED' self.pending_trade = None Clock.schedule_once(lambda dt: self._priority_scan(pairs_needed), 0) def _priority_scan(self, pairs): """Run priority scan for specific pairs.""" def scan_priority_pair(pair): try: result = self.scan_pair_method(pair) if result and result.get('setup'): self.setup_queue.add_setup(result['setup'], source="priority_scan") except Exception as e: print(f"[PRIORITY SCAN] Error scanning {pair}: {e}") # Submit to thread pool for pair in pairs[:5]: # Limit to top 5 self._thread_pool.submit(scan_priority_pair, pair) def scan_pair_method(self, pair): """Scan a single pair - standalone method for queue integration.""" import time start_time = time.time() # CHECK CACHE FIRST cache_entry = self._setup_cache.get(pair) if cache_entry: age = time.time() - cache_entry['timestamp'] if age < self._setup_cache_ttl: 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 tf_data = self.binance.fetch_multi_timeframe_data(pair) elapsed = time.time() - start_time if elapsed > 3.0: print(f"[SCAN TIMEOUT] {pair}: Multi-TF took {elapsed:.1f}s") raise TimeoutError(f"Multi-TF scan timeout: {elapsed:.1f}s") 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_val = self.all_limits_off # Calculate exhaustion score for reversal detection # Use 15m data for exhaustion calculation (good balance of responsiveness vs noise) 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_val), 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 } # === LOG SETUP TO ANALYTICS === try: if hasattr(self, 'analytics_logger'): confluence_data = { 'confluence_score': setup.get('confluence_score', 0), 'weighted_score': setup.get('weighted_score', 0), 'm3_dir': setup.get('m3_direction', ''), 'm5_dir': setup.get('m5_direction', ''), 'm15_dir': setup.get('m15_direction', ''), 'h1_dir': setup.get('h1_direction', ''), 'h2_dir': setup.get('h2_direction', ''), 'h4_dir': setup.get('h4_direction', '') } indicators = { 'rsi': setup.get('rsi', 0), 'bb_squeeze': setup.get('bb_squeeze', False), 'bb_percent_b': setup.get('bb_position', 0.5), 'supertrend': setup.get('supertrend', 'NEUTRAL'), 'supertrend_signal': setup.get('supertrend_signal', False), 'exhaustion_score': exhaustion_score } self.analytics_logger.log_setup_detected( setup=setup, confluence_data=confluence_data, indicators=indicators, taken=False # Will update to True when trade is opened ) except Exception as e: print(f"[ANALYTICS] Error logging setup: {e}") # === END ANALYTICS LOGGING === # Update cache self._setup_cache[pair] = { 'setup': setup, 'market_data': market_data, 'timestamp': time.time(), 'multi_tf': True } return { 'pair': pair, 'setup': setup, 'market_data': market_data, 'log': f"[DEBUG] {pair}: MULTI-TF SETUP", 'multi_tf': True } # Fallback to single timeframe klines = self.binance.get_klines(pair, '1h', 50) if not klines or not isinstance(klines, list) or len(klines) == 0: return {'pair': pair, 'setup': None, 'market_data': None, 'log': f"[DEBUG] {pair}: No data"} 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: return {'pair': pair, 'setup': None, 'market_data': None, 'log': f"[DEBUG] {pair}: Insufficient data"} with self._filter_lock: all_limits_val = self.all_limits_off setup = scan_setups(prices, volumes, pair, no_filter=(self.test_mode or all_limits_val), 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'): 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 } self._setup_cache[pair] = { 'setup': setup, 'market_data': market_data, 'timestamp': time.time(), 'multi_tf': False } return { 'pair': pair, 'setup': setup, 'market_data': market_data, 'log': f"[DEBUG] {pair}: SINGLE-TF SETUP", 'multi_tf': False } return {'pair': pair, 'setup': None, 'market_data': None, 'log': f"[DEBUG] {pair}: No setup"} except Exception as e: return {'pair': pair, 'setup': None, 'market_data': None, 'log': f"[DEBUG] {pair}: ERROR {e}"} # ============================================================================ # END SETUP QUEUE METHODS # ============================================================================ def build_setups_screen(self): main_container = BoxLayout(orientation='vertical', padding=dp(8), spacing=dp(8)) header_container = BoxLayout(orientation='vertical', size_hint_y=None, height=dp(70), spacing=dp(4), padding=(0, 0, dp(8), 0)) # Simple control row - buttons always visible (no more collapsible +SHOW) control_row = BoxLayout(size_hint_y=None, height=dp(40), spacing=dp(4), padding=(0, dp(4))) self.auto_refresh_toggle = StyledButton( text="[b]AUTO[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(9), radius=8 ) self.auto_refresh_toggle.bind(on_press=self.toggle_auto_refresh) control_row.add_widget(self.auto_refresh_toggle) self.test_mode_toggle = StyledButton( text="[b]TEST[/b]", markup=True, bg_color=CYAN, text_color=WHITE, font_size=sp(9), radius=8 ) self.test_mode_toggle.bind(on_press=self.toggle_test_mode) control_row.add_widget(self.test_mode_toggle) refresh_btn = StyledButton( text="[b]REFRESH[/b]", markup=True, bg_color=BLUE, text_color=WHITE, font_size=sp(9), radius=8 ) refresh_btn.bind(on_press=lambda x: self.force_market_scan()) control_row.add_widget(refresh_btn) # STOP SCAN BUTTON self.stop_scan_btn = StyledButton( text="[b]STOP[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(9), radius=8, disabled=True, opacity=0.5 ) self.stop_scan_btn.bind(on_press=self.stop_market_scan) control_row.add_widget(self.stop_scan_btn) update_btn = StyledButton( text="[b]UPDATE[/b]", markup=True, bg_color=CYAN, text_color=BLACK, font_size=sp(9), radius=8 ) update_btn.bind(on_press=lambda x: self.update_existing_setups()) control_row.add_widget(update_btn) clear_btn = StyledButton( text="[b]CLEAR[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(9), radius=8 ) clear_btn.bind(on_press=self.clear_setups) control_row.add_widget(clear_btn) export_setups_btn = StyledButton( text="[b]EXPORT[/b]", markup=True, bg_color=AMBER, text_color=BLACK, font_size=sp(9), radius=8 ) export_setups_btn.bind(on_press=self.export_setup_log) control_row.add_widget(export_setups_btn) # SNITCH BUTTON for UI issues snitch_btn = self.create_snitch_button("SETUPS") control_row.add_widget(snitch_btn) header_container.add_widget(control_row) # Status label self.setups_status_lbl = Label(text="Ready to scan", markup=True, color=GRAY, font_size=sp(10), size_hint_y=None, height=dp(0), opacity=0) # Found setups label row - CENTERED status_row = BoxLayout(size_hint_y=None, height=dp(25), spacing=dp(4), padding=(0, dp(2), 0, dp(2))) self.found_setups_lbl = Label( text="", markup=True, color=GREEN, font_size=sp(10), size_hint_x=1.0, halign='center' ) status_row.add_widget(self.found_setups_lbl) header_container.add_widget(status_row) main_container.add_widget(header_container) # ============================================ # SETUPS LIST (top setup card removed) # ============================================ setups_scroll = ScrollView() self.setups_layout = BoxLayout(orientation='vertical', size_hint_y=None, spacing=dp(10), padding=dp(4)) self.setups_layout.bind(minimum_height=self.setups_layout.setter('height')) # NOTE: Pair Manager moved to SETTINGS tab (collapsible card) # The pair manager is now a collapsible card in Settings tab for cleaner SETUPS view self.setups_container = BoxLayout(orientation='vertical', size_hint_y=None, spacing=dp(10)) self.setups_container.bind(minimum_height=self.setups_container.setter('height')) self.setups_layout.add_widget(self.setups_container) setups_scroll.add_widget(self.setups_layout) main_container.add_widget(setups_scroll) self.setups_screen = main_container def _toggle_setups_buttons(self, instance): """Toggle the setups button row between collapsed and expanded.""" try: # Safety check - ensure container exists if not hasattr(self, 'setups_btn_container') or self.setups_btn_container is None: print("[ERROR] setups_btn_container not found!") return self._setups_btn_expanded = not getattr(self, '_setups_btn_expanded', False) if self._setups_btn_expanded: instance.text = "[b]- HIDE[/b]" self.setups_btn_container.height = dp(55) self.setups_btn_container.opacity = 1 # Force visibility of all children for child in self.setups_btn_container.children: child.height = dp(40) child.opacity = 1 for subchild in child.children: subchild.opacity = 1 print("[UI] Controls expanded - buttons should be visible") else: instance.text = "[b]+ SHOW[/b]" self.setups_btn_container.height = dp(0) self.setups_btn_container.opacity = 0 print("[UI] Controls collapsed") except Exception as e: print(f"[ERROR] Toggle setups buttons failed: {e}") import traceback traceback.print_exc() def show_info_popup(self, title, text, size_hint=(0.85, None), height=dp(250)): from kivy.uix.popup import Popup from kivy.uix.scrollview import ScrollView content = BoxLayout(orientation='vertical', spacing=dp(10), padding=dp(15)) title_lbl = Label( text=f"[b]{title}[/b]", markup=True, color=GOLD, font_size=sp(14), size_hint_y=None, height=dp(30) ) content.add_widget(title_lbl) scroll = ScrollView(size_hint_y=1) text_lbl = Label( text=text, markup=True, color=WHITE, font_size=sp(11), size_hint_y=None, text_size=(Window.width * 0.75, None), halign='left', valign='top' ) text_lbl.bind(texture_size=lambda lbl, size: setattr(lbl, 'height', size[1])) scroll.add_widget(text_lbl) content.add_widget(scroll) close_btn = StyledButton( text="[b]GOT IT[/b]", markup=True, bg_color=BLUE, text_color=WHITE, font_size=sp(12), radius=10, size_hint_y=None, height=dp(40) ) content.add_widget(close_btn) content.add_widget(self.dual_entry_btn) popup = Popup( title='', content=content, size_hint=size_hint, height=height, background_color=(0.1, 0.1, 0.15, 0.95), separator_color=GOLD, auto_dismiss=True ) close_btn.bind(on_press=popup.dismiss) popup.open() def create_info_label(self, text, info_text, title=None, size_hint_x=None, font_size=sp(9), color=GRAY): from kivy.uix.button import Button container = BoxLayout(size_hint_x=size_hint_x, spacing=dp(4)) lbl = Label( text=text, color=color, font_size=font_size, size_hint_x=0.85 if size_hint_x else None, halign='left' ) container.add_widget(lbl) info_btn = Button( text="[b](i)[/b]", markup=True, background_color=(0, 0, 0, 0), color=CYAN, font_size=sp(10), size_hint_x=0.15, size_hint_y=None, height=dp(24) ) info_btn.bind(on_press=lambda x: self.show_info_popup( title or text.replace(":", ""), info_text )) container.add_widget(info_btn) return container def build_rob_bot_screen(self): """Clean ROB-BOT tab with top control bar and essential cards only.""" self.rob_bot_screen = ScrollView() with self.rob_bot_screen.canvas.before: Color(0.08, 0.09, 0.11, 1) self.rob_bot_screen.rect = Rectangle(pos=self.rob_bot_screen.pos, size=self.rob_bot_screen.size) self.rob_bot_screen.bind(pos=lambda i, v: setattr(self.rob_bot_screen.rect, 'pos', v)) self.rob_bot_screen.bind(size=lambda i, v: setattr(self.rob_bot_screen.rect, 'size', v)) main_layout = BoxLayout(orientation='vertical', size_hint_y=None, spacing=dp(6), padding=dp(6)) main_layout.bind(minimum_height=main_layout.setter('height')) # ============================================ # TOP CONTROL BAR - All essentials in one card # ============================================ control_card = BorderedCard(size_hint_y=None, height=dp(140)) control_card.orientation = 'vertical' # Row 1: Engage/Stop button + Status row1 = BoxLayout(size_hint_y=None, height=dp(44), spacing=dp(6)) self.engage_btn = StyledButton( text="[b]START BOT[/b]" if not self.bot_engaged else "[b]STOP BOT[/b]", markup=True, bg_color=DARK_GREEN if not self.bot_engaged else DARK_RED, text_color=WHITE, font_size=sp(13), radius=10, size_hint_x=0.4 ) self.engage_btn.bind(on_press=self.toggle_bot_engaged) row1.add_widget(self.engage_btn) # Status indicators status_box = BoxLayout(orientation='vertical', size_hint_x=0.6) self.bot_status_line1 = Label( text=f"Mode: {'ROBOT' if self.robot_scanner_mode else 'MANUAL'} | Paper: {'ON' if self.paper_mode else 'OFF'}", color=CYAN, font_size=sp(9), size_hint_y=0.5 ) self.bot_status_line2 = Label( text=f"Order: {self.order_type} | Dir: {self.trade_direction}", color=AMBER, font_size=sp(9), size_hint_y=0.5 ) status_box.add_widget(self.bot_status_line1) status_box.add_widget(self.bot_status_line2) row1.add_widget(status_box) control_card.add_widget(row1) # Row 2: Filter + R/R row2 = BoxLayout(size_hint_y=None, height=dp(38), spacing=dp(6)) # Unified Filter Dropdown filter_box = BoxLayout(size_hint_x=0.5, spacing=dp(4)) filter_box.add_widget(Label(text="Filter:", color=WHITE, font_size=sp(10), size_hint_x=0.3)) self.filter_spinner = Spinner( text=self.filter_mode, values=['OFF', 'A_ONLY', 'MIN_B', 'MIN_C'], size_hint_x=0.7, background_color=CARD_BG, color=WHITE, font_size=sp(10) ) self.filter_spinner.bind(text=self.on_filter_changed) filter_box.add_widget(self.filter_spinner) row2.add_widget(filter_box) # Min R/R rr_box = BoxLayout(size_hint_x=0.5, spacing=dp(4)) rr_box.add_widget(Label(text="Min R/R:", color=WHITE, font_size=sp(10), size_hint_x=0.4)) self.rr_spinner = Spinner( text=f"{self.min_rr_ratio:.1f}", values=['1.0', '1.5', '2.0', '2.5', '3.0'], size_hint_x=0.6, background_color=CARD_BG, color=WHITE, font_size=sp(10) ) self.rr_spinner.bind(text=self.on_rr_changed) rr_box.add_widget(self.rr_spinner) row2.add_widget(rr_box) control_card.add_widget(row2) # Spacer before MODES button control_card.add_widget(BoxLayout(size_hint_y=None, height=dp(8))) # Row 3: MODES dropdown button (centered, not full width) modes_btn_container = BoxLayout(size_hint_y=None, height=dp(28)) modes_btn = StyledButton( text="[b]MODES[/b]", markup=True, bg_color=GOLD, text_color=BLACK, font_size=sp(11), radius=8, size_hint=(None, None), size=(dp(100), dp(28)), pos_hint={'center_x': 0.5} ) modes_btn.bind(on_press=self.show_modes_menu) modes_btn_container.add_widget(modes_btn) control_card.add_widget(modes_btn_container) main_layout.add_widget(control_card) # ============================================ # BOT LOG # ============================================ log_card = BorderedCard(size_hint_y=None, height=dp(280)) log_card.orientation = 'vertical' log_header = BoxLayout(size_hint_y=None, height=dp(32), spacing=dp(4)) log_header.add_widget(EmojiLabel(text="[b]BOT LOG[/b]", markup=True, color=GOLD, font_size=sp(12), size_hint_x=0.4)) export_btn = StyledButton(text="[b]EXPORT[/b]", markup=True, bg_color=BLUE, text_color=WHITE, font_size=sp(9), radius=6, size_hint_x=0.15) export_btn.bind(on_press=self.export_bot_log) log_header.add_widget(export_btn) # Send logs button send_logs_btn = StyledButton(text="[b]SEND[/b]", markup=True, bg_color=CYAN, text_color=BLACK, font_size=sp(9), radius=6, size_hint_x=0.15) send_logs_btn.bind(on_press=self.send_logs_to_kimi) log_header.add_widget(send_logs_btn) clear_btn = StyledButton(text="[b]CLEAR[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(9), radius=6, size_hint_x=0.15) clear_btn.bind(on_press=self.clear_bot_log) log_header.add_widget(clear_btn) # SNITCH BUTTON for UI issues snitch_btn = self.create_snitch_button("ROB-BOT") log_header.add_widget(snitch_btn) log_card.add_widget(log_header) self.bot_log = BoxLayout(orientation='vertical', size_hint_y=None, spacing=dp(4), padding=dp(4)) self.bot_log.bind(minimum_height=self.bot_log.setter('height')) log_scroll = ScrollView(size_hint_y=0.88) log_scroll.add_widget(self.bot_log) log_card.add_widget(log_scroll) main_layout.add_widget(log_card) Clock.schedule_interval(self.update_regime_display, 30) self.rob_bot_screen.add_widget(main_layout) def show_modes_menu(self, instance): """Show modes dropdown menu with regime status.""" content = BoxLayout(orientation='vertical', spacing=dp(4), padding=dp(8)) # Force refresh of current states optimus_active = getattr(self, 'optimus_override', False) bigbrain_active = getattr(self, 'big_brain_enabled', False) scalping_active = getattr(self, 'scalping_mode_enabled', True) alpha_active = getattr(self, 'alpha_protocol_enabled', False) print(f"[MODES MENU] Optimus: {optimus_active}, BigBrain: {bigbrain_active}, Scalping: {scalping_active}") # Add Market Regime status card at top regime_status_card = BorderedCard(size_hint_y=None, height=dp(60), padding=dp(6)) regime_status_card.orientation = 'horizontal' regime_status_card.add_widget(Label( text="[b]REGIME[/b]", markup=True, color=GOLD, font_size=sp(11), size_hint_x=0.25 )) # Get current regime status regime_text = "Analyzing..." if hasattr(self, 'last_scan_results') and self.last_scan_results: setups = [s for s in self.last_scan_results if s.get('detected')] if setups: directions = [s.get('direction') for s in setups] long_count = directions.count('LONG') short_count = directions.count('SHORT') if long_count > short_count: regime_text = f"[color=00ff00]BULLISH[/color] ({long_count}L/{short_count}S)" elif short_count > long_count: regime_text = f"[color=ff0000]BEARISH[/color] ({long_count}L/{short_count}S)" else: regime_text = f"[color=ffff00]NEUTRAL[/color] ({long_count}L/{short_count}S)" regime_lbl = Label( text=regime_text, markup=True, color=WHITE, font_size=sp(10), size_hint_x=0.5 ) regime_status_card.add_widget(regime_lbl) # Toggle button for regime filter regime_toggle = StyledButton( text="[b]ON[/b]" if self.regime_filter_enabled else "[b]OFF[/b]", markup=True, bg_color=GREEN if self.regime_filter_enabled else GRAY, text_color=WHITE, font_size=sp(9), radius=6, size_hint_x=0.25 ) regime_toggle.bind(on_press=lambda x: self.toggle_regime_filter(x)) regime_status_card.add_widget(regime_toggle) content.add_widget(regime_status_card) # Get current volume threshold for display vol_thresholds = {0: "None", 250000: "250K", 500000: "500K", 750000: "750K", 1000000: "1M"} vol_display = vol_thresholds.get(getattr(self, 'volume_threshold', 0), "None") # Get Money Maker status mm_active = getattr(self, 'money_maker_mode', False) mm_status = "[CASH]ACTIVE" if mm_active else "OFF" # Get Falling Knife status fk_active = getattr(self, 'falling_knife_enabled', False) or (hasattr(self, 'falling_knife') and self.falling_knife and self.falling_knife.is_enabled()) fk_status = "[SWORD]️ACTIVE" if fk_active else "OFF" # Store popup reference for callbacks that need to dismiss first self._modes_popup = None # Get DUAL ENTRY status dual_active = getattr(self, 'dual_entry_enabled', True) dual_status = "ON" if dual_active else "OFF" # Get AUTO TRADE status auto_trade_active = getattr(self, 'auto_enable_trading', False) auto_trade_status = "ON" if auto_trade_active else "OFF" # Get AUTO SEND LOGS status auto_send_logs = getattr(self, 'auto_send_logs_enabled', False) auto_send_status = "ON" if auto_send_logs else "OFF" modes = [ (f"[TRACTOR] Money Bulldozer: {mm_status}", 'money_maker', False), (f"[SWORD]️ Falling Knife: {fk_status}", 'falling_knife', False), (f"[SHUFFLE] DUAL ENTRY: {dual_status}", 'dual_entry', dual_active), (f"🤖 AUTO TRADE: {auto_trade_status}", 'auto_trade', auto_trade_active), (f"[UPLOAD] AUTO SEND LOGS (30min): {auto_send_status}", 'auto_send_logs', auto_send_logs), ("Big Brain", self._toggle_big_brain, bigbrain_active), ("Optimus Brain", self._toggle_optimus_override, optimus_active), ("Scalping", self.toggle_scalping_mode, scalping_active), ("Order Type", self.show_order_type_menu, False), ("Trade Direction", self.show_direction_menu, False), ("Alpha Protocol", self.toggle_alpha_protocol, alpha_active), ("Risk Mgmt", self.show_risk_menu, False), (f"Volume: {vol_display}", self.show_volume_threshold_menu, False), ("[WRENCH] FIX DUAL ENTRY", 'fix_dual_entry', False), ("VIEW BLOCKED", 'view_blocked', False), ] for name, callback, active in modes: btn = StyledButton( text=f"[b]{'[ON] ' if active else '[OFF] '}{name}[/b]", markup=True, bg_color=GREEN if active else CARD_BG, text_color=WHITE if active else GRAY, font_size=sp(11), radius=6, size_hint_y=None, height=dp(36) ) if callback == 'money_maker': # Special handling for Money Maker - dismiss first, then show confirmation btn.bind(on_press=lambda x: (self._modes_popup.dismiss(), self._show_money_maker_confirmation())) elif callback == 'falling_knife': # Special handling for Falling Knife - dismiss first, then show confirmation btn.bind(on_press=lambda x: (self._modes_popup.dismiss(), self._show_fk_enable_confirmation())) elif callback == 'dual_entry': # Special handling for DUAL ENTRY - toggle and refresh menu btn.bind(on_press=lambda x: (self.toggle_dual_entry(x), self._modes_popup.dismiss(), self.show_modes_menu(x))) elif callback == 'auto_trade': # Special handling for AUTO TRADE - toggle and refresh menu btn.bind(on_press=lambda x: (self.toggle_auto_trade_mode(x), self._modes_popup.dismiss(), self.show_modes_menu(x))) elif callback == 'auto_send_logs': # Special handling for AUTO SEND LOGS - toggle and refresh menu btn.bind(on_press=lambda x: (self.toggle_auto_send_logs(x), self._modes_popup.dismiss(), self.show_modes_menu(x))) elif callback == 'fix_dual_entry': # Special handling for FIX DUAL ENTRY - run migration btn.bind(on_press=lambda x: (self._modes_popup.dismiss(), self._manual_fix_dual_entry(), self.show_modes_menu(x))) elif callback == 'view_blocked': # Show blocked pairs btn.bind(on_press=lambda x: (self._modes_popup.dismiss(), self._show_blocked_pairs())) else: btn.bind(on_press=lambda x, cb=callback: (cb(x), self._modes_popup.dismiss())) content.add_widget(btn) self._modes_popup = Popup(title='[b]MODES[/b]', content=content, size_hint=(0.85, 0.7), background_color=DARK_BG, title_color=GOLD) self._modes_popup.open() def on_filter_changed(self, spinner, text): """Handle filter spinner change.""" self.set_filter_mode(text) def on_rr_changed(self, spinner, text): """Handle R/R spinner change.""" try: self.min_rr_ratio = float(text) self.save_settings_ui(None) except: pass def show_order_type_menu(self, instance): """Show order type submenu.""" content = BoxLayout(orientation='vertical', spacing=dp(4), padding=dp(8)) for ot in ORDER_TYPES: btn = StyledButton( text=f"[b]{ot}[/b]", markup=True, bg_color=GOLD if ot == self.order_type else CARD_BG, text_color=BLACK if ot == self.order_type else WHITE, font_size=sp(11), radius=6, size_hint_y=None, height=dp(40) ) btn.bind(on_press=lambda x, t=ot: (self.select_order_type(t), popup.dismiss())) content.add_widget(btn) popup = Popup(title='[b]ORDER TYPE[/b]', content=content, size_hint=(0.7, 0.5), background_color=DARK_BG, title_color=GOLD) popup.open() def show_direction_menu(self, instance): """Show trade direction submenu.""" content = BoxLayout(orientation='vertical', spacing=dp(4), padding=dp(8)) for d in TRADE_DIRECTIONS: btn = StyledButton( text=f"[b]{d}[/b]", markup=True, bg_color=CYAN if d == self.trade_direction else CARD_BG, text_color=BLACK if d == self.trade_direction else WHITE, font_size=sp(11), radius=6, size_hint_y=None, height=dp(40) ) btn.bind(on_press=lambda x, td=d: (self.select_trade_direction(td), popup.dismiss())) content.add_widget(btn) popup = Popup(title='[b]DIRECTION[/b]', content=content, size_hint=(0.7, 0.4), background_color=DARK_BG, title_color=GOLD) popup.open() def show_risk_menu(self, instance): """Show risk management submenu.""" content = BoxLayout(orientation='vertical', spacing=dp(4), padding=dp(8)) risks = [ ("Circuit Breaker", self.toggle_circuit_breaker, getattr(self, 'circuit_breaker_enabled', True)), ("Regime Filter", self.toggle_regime_filter, self.regime_filter_enabled), ("DXY Filter", self.toggle_dxy_filter, getattr(self, 'dxy_filter_enabled', True)), ("Time Restrict", self.toggle_time_restrictions, self.time_restrictions_enabled), ] for name, callback, active in risks: btn = StyledButton( text=f"[b]{'[ON] ' if active else '[OFF] '}{name}[/b]", markup=True, bg_color=GREEN if active else CARD_BG, text_color=WHITE if active else GRAY, font_size=sp(11), radius=6, size_hint_y=None, height=dp(36) ) btn.bind(on_press=lambda x, cb=callback: cb(x)) content.add_widget(btn) popup = Popup(title='[b]RISK MANAGEMENT[/b]', content=content, size_hint=(0.8, 0.5), background_color=DARK_BG, title_color=GOLD) popup.open() def show_volume_threshold_menu(self, instance): """Show volume threshold submenu with confirmation.""" content = BoxLayout(orientation='vertical', spacing=dp(4), padding=dp(8)) thresholds = [ (0, "None - All pairs allowed"), (250000, "250K - Very relaxed"), (500000, "500K - Relaxed"), (750000, "750K - Balanced"), (1000000, "1M - Strict liquidity"), ] current_threshold = getattr(self, 'volume_threshold', 0) for val, label in thresholds: is_current = val == current_threshold btn = StyledButton( text=f"[b]{'[[OK]] ' if is_current else ''}{label}[/b]", markup=True, bg_color=CYAN if is_current else CARD_BG, text_color=BLACK if is_current else WHITE, font_size=sp(11), radius=6, size_hint_y=None, height=dp(40) ) btn.bind(on_press=lambda x, v=val, l=label: (self._confirm_volume_threshold(v, l, main_popup), main_popup.dismiss())) content.add_widget(btn) main_popup = Popup(title='[b]VOLUME THRESHOLD[/b]', content=content, size_hint=(0.85, 0.6), background_color=DARK_BG, title_color=GOLD) main_popup.open() def _confirm_volume_threshold(self, value, label, parent_popup): """Show confirmation dialog for volume threshold change.""" old_val = getattr(self, 'volume_threshold', 0) old_label = {0: "None", 250000: "250K", 500000: "500K", 750000: "750K", 1000000: "1M"}.get(old_val, "None") content = BoxLayout(orientation='vertical', spacing=dp(8), padding=dp(12)) # Warning message msg = Label( text=f"[b]Change Volume Threshold?[/b]\n\nFrom: {old_label}\nTo: {label.split(' - ')[0]}\n\nThis affects which pairs the bot will trade based on 24h volume.", markup=True, color=WHITE, font_size=sp(12), halign='center' ) content.add_widget(msg) # Buttons row buttons = BoxLayout(size_hint_y=None, height=dp(50), spacing=dp(10)) 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=lambda x: confirm_popup.dismiss()) buttons.add_widget(cancel_btn) confirm_btn = StyledButton( text="[b]CONFIRM[/b]", markup=True, bg_color=DARK_GREEN, text_color=WHITE, font_size=sp(12), radius=8 ) confirm_btn.bind(on_press=lambda x: (self._set_volume_threshold(value, label), confirm_popup.dismiss())) buttons.add_widget(confirm_btn) content.add_widget(buttons) confirm_popup = Popup( title='[b][WARN]️ CONFIRM[/b]', content=content, size_hint=(0.8, 0.4), background_color=DARK_BG, title_color=AMBER ) confirm_popup.open() def _set_volume_threshold(self, value, label): """Actually set the volume threshold.""" self.volume_threshold = value self.log_activity(f"[b]VOLUME THRESHOLD:[/b] Changed to {label}", "WARN") print(f"[VOLUME] Threshold set to {value:,} ({label})") # Force fresh scan with new threshold self.validation_timeouts.clear() self.force_market_scan() def _toggle_big_brain(self, instance=None): """Toggle Big Brain mode.""" self.big_brain_enabled = not getattr(self, 'big_brain_enabled', False) # RESPECT OFF SWITCH: Track when user turns Big Brain OFF self.big_brain_user_disabled = not self.big_brain_enabled if self.big_brain_user_disabled: print("[BIG BRAIN] User disabled - auto-start blocked until manually enabled") self.log_activity(f"Big Brain: {'ON' if self.big_brain_enabled else 'OFF'}", "INFO") def toggle_alpha_protocol(self, instance=None): """Toggle Alpha Protocol.""" self.alpha_protocol_enabled = not getattr(self, 'alpha_protocol_enabled', False) self.log_activity(f"Alpha Protocol: {'ON' if self.alpha_protocol_enabled else 'OFF'}", "INFO") def set_setup_search_pause(self, paused): if paused == self.setup_search_paused: return self.setup_search_paused = paused if paused: self._pre_pause_auto_refresh = self.auto_refresh self.auto_refresh = False self.log_activity("[b]SETUP SEARCH:[/b] PAUSED - Bot full (max positions reached)", "WARN") else: self.auto_refresh = self._pre_pause_auto_refresh self.log_activity("[b]SETUP SEARCH:[/b] RESUMED - Position slot available", "SUCCESS") Clock.schedule_once(lambda dt: self.force_market_scan(), 1) def _check_pause_state(self): open_count = len([p for p in self.position_manager.positions if p['status'] == 'OPEN']) if open_count >= self.max_simultaneous: if not self.setup_search_paused: self.set_setup_search_pause(True) else: if self.setup_search_paused: self.set_setup_search_pause(False) def passes_all_bot_filters(self, setup): with self._filter_lock: all_limits_off = self.all_limits_off if all_limits_off: return True, "ALL LIMITS OFF - Setup passes" setup_direction = setup.get('direction', 'LONG') if self.trade_direction != "BOTH" and setup_direction != self.trade_direction: return False, f"Direction mismatch: {setup_direction} (filter set to {self.trade_direction})" if self.regime_filter_enabled and self.regime_use_advanced and hasattr(self, 'regime_detector'): # Regime check removed in lite version - skip this filter pass sentinel_passed, sentinel_msg = self.run_sentinel_analysis(setup) if not sentinel_passed: return False, sentinel_msg min_grade_score = grade_to_score(self.min_trade_grade) setup_score = setup.get('score', 0) if setup_score < min_grade_score: return False, f"Grade too low: {setup.get('grade', '?')} (need {self.min_trade_grade}+)" rr = setup.get('rr_ratio', 0) if rr < self.min_rr_ratio: return False, f"R/R too low: {rr:.1f} (need {self.min_rr_ratio}+)" if self.time_restrictions_enabled and not self.is_trading_window_open(): return False, "Trading window closed" return True, "All filters passed" def _create_thor_signal_card(self, signal): card = BorderedCard(size_hint_y=None, height=dp(120)) card.orientation = 'vertical' card.padding = dp(8) card.spacing = dp(4) pair = signal['pair'] direction = signal['direction'] sig_type = signal['signal'] grade = signal['grade'] strength = signal['strength'] volume = signal['volume_ratio'] color = GREEN if direction == 'LONG' else RED trap_warning = None if self.bulltrap_enabled and hasattr(self, 'bulltrap_detector'): trap_check = self.bulltrap_detector.check_trap(pair) if trap_check['is_trap']: if (direction == 'LONG' and trap_check['type'] == 'BEARISH_BULLTRAP') or \ (direction == 'SHORT' and trap_check['type'] == 'BULLISH_BEARTRAP'): trap_warning = trap_check header = BoxLayout(size_hint_y=0.3) header.add_widget(Label( text=f"[b]{pair}[/b]", markup=True, color=GOLD, font_size=sp(13), size_hint_x=0.4 )) header.add_widget(Label( text=f"[b]{sig_type}[/b]", markup=True, color=color, font_size=sp(11), size_hint_x=0.35 )) header.add_widget(Label( text=f"[b]{grade}[/b]", markup=True, color=AMBER, font_size=sp(13), size_hint_x=0.25 )) card.add_widget(header) details = BoxLayout(size_hint_y=0.25) details.add_widget(Label( text=f"Vol: {volume:.1f}x | Str: {strength:.0f}", color=GRAY, font_size=sp(9), size_hint_x=0.5 )) details.add_widget(Label( text=f"Dir: {direction}", color=color, font_size=sp(10), size_hint_x=0.5 )) card.add_widget(details) if trap_warning: trap_row = BoxLayout(size_hint_y=0.2) emoji = "[ALERT]" if trap_warning['score'] >= 80 else "[WARN]" trap_text = f"{emoji} {trap_warning['type'].replace('_', ' ')}: {trap_warning['score']}/100" trap_row.add_widget(Label( text=f"[b]{trap_text}[/b]", markup=True, color=(1, 0.3, 0.3, 1), font_size=sp(9) )) card.add_widget(trap_row) trade_btn = StyledButton( text="[b][WARN]️ TRADE[/b]" if trap_warning else "[b]TRADE[/b]", markup=True, bg_color=(0.8, 0.3, 0.3, 1) if trap_warning else color, text_color=WHITE, font_size=sp(10), radius=6, size_hint_y=0.25 ) trade_btn.bind(on_press=lambda x, s=signal: self.execute_thor_trade(s)) card.add_widget(trade_btn) return card def scan_thor_signals(self): self.log_activity("[b]THOR:[/b] Scanning for volume + trend signals...") pairs_to_scan = ['BTCUSDC'] if DEBUG_BTC_ONLY else self.get_scan_pairs()[:10] signals = self.thor_indicator.scan_all_pairs(pairs=pairs_to_scan) self.update_thor_display() count = len(signals) self.log_activity(f"[b]THOR:[/b] Found {count} signals") def toggle_bulltrap(self, instance): self.bulltrap_enabled = not self.bulltrap_enabled instance.text = "[b]ON[/b]" if self.bulltrap_enabled else "[b]OFF[/b]" instance.set_bg_color(GREEN if self.bulltrap_enabled else GRAY) if self.bulltrap_enabled: self.log_activity("[b]BULLTRAP:[/b] ENABLED - Fake breakout detection active") else: self.log_activity("[b]BULLTRAP:[/b] DISABLED - Trap detection off") def execute_thor_trade(self, signal): pair = signal['pair'] direction = signal['direction'] if self.thor_btc_correlation and not self.thor_indicator.is_btc_aligned(direction): self.log_activity(f"[b]THOR:[/b] {pair} skipped - BTC not aligned with {direction}") return if self.bulltrap_enabled and hasattr(self, 'bulltrap_detector'): is_trap, trap_data = self.bulltrap_detector.should_block_trade( pair, direction, self.bulltrap_min_score ) if is_trap: warning = self.bulltrap_detector.format_warning(trap_data) self.log_activity(f"[b]🪤 BULLTRAP BLOCKED:[/b] {pair} {direction}") self.log_activity(f" {warning}") return self.log_activity(f"[b]THOR TRADE:[/b] {pair} {direction} - {signal['signal']}") setup = { 'pair': pair, 'direction': direction, 'entry': signal['price'], 'stop_loss': signal['price'] * 0.98 if direction == 'LONG' else signal['price'] * 1.02, 'take_profit1': signal['price'] * 1.03 if direction == 'LONG' else signal['price'] * 0.97, 'rr_ratio': 1.5, 'score': signal['strength'], 'grade': signal['grade'], 'thor_signal': True } self.execute_trade(setup) def build_positions_screen(self): self.positions_screen = BoxLayout(orientation='vertical', padding=dp(8), spacing=dp(8)) header_card = BoxLayout(orientation='vertical', size_hint_y=None, height=dp(110), spacing=dp(2), padding=(dp(4), dp(2), dp(4), dp(4))) with header_card.canvas.before: Color(*LIGHT_BG) self.pos_header_rect = Rectangle(pos=header_card.pos, size=header_card.size) Color(*GOLD) self.pos_header_border = Line(rounded_rectangle=(header_card.x, header_card.y, header_card.width, header_card.height, dp(12)), width=dp(2)) header_card.bind(pos=self._update_pos_header, size=self._update_pos_header) # Title row with snitch button title_row = BoxLayout(size_hint_y=None, height=dp(28)) self.active_pos_header = EmojiLabel(text="[b]ACTIVE POSITIONS (0)[/b]", markup=True, color=GOLD, font_size=sp(15), size_hint_x=0.8) title_row.add_widget(self.active_pos_header) # SNITCH BUTTON for UI issues snitch_btn = self.create_snitch_button("POSITIONS") title_row.add_widget(snitch_btn) header_card.add_widget(title_row) self.stats_lbl = Label(text="T:0 W:0% O:0/2", markup=True, color=WHITE, font_size=sp(10), size_hint_y=None, height=dp(20), text_size=(None, None)) header_card.add_widget(self.stats_lbl) close_all_btn = StyledButton( text="[b]CLOSE ALL[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(10), radius=10, size_hint_y=None, height=dp(32), size_hint_x=0.5, pos_hint={'center_x': 0.5} ) close_all_btn.bind(on_press=self.close_all_positions)