{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 stop requested if getattr(self, '_scan_stop_requested', False): self.log_activity(f"[SCAN STOPPED] User requested stop", "WARN") cancelled_count = len(future_to_pair) - len(all_setups) debug_logs.append(f"[DEBUG] === SCAN STOPPED by user ({cancelled_count} pairs cancelled) ===") break # 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] [OK] 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] [OK] 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][OK] 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() # Enable stop button self.stop_scan_btn.disabled = False self.stop_scan_btn.opacity = 1.0 def stop_market_scan(self, instance=None): """Stop the current market scan.""" self._scan_stop_requested = True self.log_activity("[b]STOP REQUESTED:[/b] Cancelling scan...", "WARN") # Disable stop button self.stop_scan_btn.disabled = True self.stop_scan_btn.opacity = 0.5 # Mark scan as complete to allow new scans self._scan_completed = True self.setups_status_lbl.text = "SCAN STOPPED" self.setups_status_lbl.color = DARK_RED 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 = "" # ============================================ # TRIGGER TRADE CHECK AFTER SCAN COMPLETES # ============================================ if results and len(results) > 0: print(f"[SCAN COMPLETE] Triggering trade check for {len(results)} setups") self.log_activity(f"[b]SCAN:[/b] {len(results)} setup(s) ready - triggering trade check", "INFO") # Schedule trade check after UI updates from kivy.clock import Clock Clock.schedule_once(lambda dt: self.check_and_enter_trade_threaded(), 0.5) 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 = '[OK]' if tf_align.get('15m') else '○' h1_ok = '[OK]' if tf_align.get('1h') else '○' h4_ok = '[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 (relaxed to 5% for testing) if direction == 'LONG': if current_price > entry_price * 1.05: # Was 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.95: # Was 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 $500K 24h volume) === volume_24h = self.market_data.get(pair, {}).get('volume_24h', 0) if volume_24h < 100_000: # Lowered from 750K for testing self.log_activity(f"[VALIDATION] {pair}: Volume ${volume_24h:,.0f} below $100K minimum", "WARN") # Don't block - just warn # return None # === 3. FUNDING RATE CHECK === funding_data = self.binance.get_funding_rate(pair) if funding_data: funding_rate = funding_data.get('fundingRate', 0) funding_pct = funding_rate * 100 # Convert to percentage # Check if funding is extreme if abs(funding_pct) > 0.1: # >0.1% funding rate is extreme direction = setup.get('direction', 'LONG') # For LONG: avoid high positive funding (expensive to hold) # For SHORT: avoid high negative funding (expensive to hold) if (direction == 'LONG' and funding_pct > 0.1) or (direction == 'SHORT' and funding_pct < -0.1): self.log_activity(f"[VALIDATION] {pair}: Extreme funding {funding_pct:+.3f}% - {direction} expensive", "WARN") # Don't reject, but warn and reduce grade setup['grade'] = reduce_grade(setup.get('grade', 'B'), 2) setup['funding_penalty'] = True else: self.log_activity(f"[VALIDATION] {pair}: Favorable funding {funding_pct:+.3f}% for {direction}", "INFO") # === 4. ORDER BOOK LIQUIDITY CHECK === orderbook_analysis = self.binance.analyze_orderbook(pair, depth_limit=50, current_price=current_price) if orderbook_analysis: liquidity_score = orderbook_analysis.get('liquidity_score', 0) bid_ask_ratio = orderbook_analysis.get('bid_ask_ratio', 1.0) direction = setup.get('direction', 'LONG') # Check liquidity if liquidity_score < 30: self.log_activity(f"[VALIDATION] {pair}: Poor liquidity (score {liquidity_score}/100)", "ERROR") return None elif liquidity_score < 50: self.log_activity(f"[VALIDATION] {pair}: Low liquidity (score {liquidity_score}/100)", "WARN") # Check bid/ask imbalance if direction == 'LONG' and bid_ask_ratio < 0.5: self.log_activity(f"[VALIDATION] {pair}: Weak bids (ratio {bid_ask_ratio:.2f}) - {direction} risky", "WARN") setup['grade'] = reduce_grade(setup.get('grade', 'B'), 1) elif direction == 'SHORT' and bid_ask_ratio > 2.0: self.log_activity(f"[VALIDATION] {pair}: Weak asks (ratio {bid_ask_ratio:.2f}) - {direction} risky", "WARN") setup['grade'] = reduce_grade(setup.get('grade', 'B'), 1) # Store for display setup['liquidity_score'] = liquidity_score setup['bid_ask_ratio'] = bid_ask_ratio # === 5. 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 # === OPTIMUS SMART ENTRY ADJUSTMENT === # Apply adjusted entry price if calculated and reasonable adjusted_entry = market_params.get('adjusted_entry') entry_adjustment_reason = market_params.get('entry_adjustment_reason', '') indicators_aligned = market_params.get('indicators_aligned', False) if adjusted_entry and adjusted_entry > 0: original_entry = setup.get('entry', 0) price_diff_pct = abs(adjusted_entry - original_entry) / original_entry * 100 # Only apply if difference is meaningful (0.1% to 1%) if 0.1 <= price_diff_pct <= 1.0: # Adjust entry old_entry = setup['entry'] setup['entry'] = round(adjusted_entry, 4) # Adjust SL and TP proportionally to maintain R/R if old_entry > 0: old_sl = setup.get('stop_loss', 0) old_tp = setup.get('take_profit1', 0) if old_sl > 0: sl_distance = abs(old_entry - old_sl) if direction == 'LONG': setup['stop_loss'] = round(adjusted_entry - sl_distance, 4) else: setup['stop_loss'] = round(adjusted_entry + sl_distance, 4) if old_tp > 0: tp_distance = abs(old_tp - old_entry) if direction == 'LONG': setup['take_profit1'] = round(adjusted_entry + tp_distance, 4) else: setup['take_profit1'] = round(adjusted_entry - tp_distance, 4) # Log the adjustment better_worse = "better" if (direction == 'LONG' and adjusted_entry < original_entry) or (direction == 'SHORT' and adjusted_entry > original_entry) else "adjusted" indicators_status = "[ALIGNED]" if indicators_aligned else "[CAUTION]" self.log_activity( f"[OPTIMUS: ENTRY] {pair} {better_worse.upper()} ${original_entry:.4f} -> ${adjusted_entry:.4f} " f"({price_diff_pct:.2f}% | {indicators_status}) | {entry_adjustment_reason}", "SUCCESS" if indicators_aligned else "WARN" ) else: self.log_activity( f"[OPTIMUS: ENTRY] {pair} Skipped adjustment ({price_diff_pct:.2f}% difference)", "INFO" ) # === END OPTIMUS SMART ENTRY === position = self.position_manager.open_position(setup, available, self.paper_mode, self.binance, market_params=market_params) if position is None: self.log_activity(f"[DEBUG] open_position returned None - trade failed", "ERROR") else: self.log_activity(f"[DEBUG] open_position returned position ID {position.get('id', 'unknown')}", "SUCCESS") 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): # Throttle: skip if ran less than 10 seconds ago now = time.time() last_update = getattr(self, '_last_positions_display_update', 0) if now - last_update < 10: return self._last_positions_display_update = now # Cloud push throttled separately (every 30s max) last_cloud_push = getattr(self, '_last_cloud_push', 0) if hasattr(self, 'cloud') and now - last_cloud_push >= 30: self._last_cloud_push = now # Push to cloud in background thread to avoid UI lag import threading threading.Thread(target=lambda: self.cloud.push_state(self), daemon=True).start() 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") # === DUAL ENTRY AUTO-FIX === # Ensure all positions have dual entry fields for display if 'dual_entry' not in pos or pos.get('dual_entry') is None: pos['dual_entry'] = True repairs.append("DE=TRUE") if 'dual_entry_part' not in pos or pos.get('dual_entry_part') is None: pos['dual_entry_part'] = 'first' repairs.append("DE_PART=first") if 'tranche2_filled' not in pos: pos['tranche2_filled'] = False repairs.append("DE_T2=FALSE") if 'dual_entry_trigger' not in pos or pos.get('dual_entry_trigger', 0) == 0: if direction == 'LONG': pos['dual_entry_trigger'] = entry * 1.005 else: pos['dual_entry_trigger'] = entry * 0.995 repairs.append(f"DE_TRIG={pos['dual_entry_trigger']:.4f}") 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 = "" dual_entry_enabled = getattr(self, 'dual_entry_enabled', True) has_dual_part = pos.get('dual_entry_part') # Show dual entry status if position has dual entry data OR if dual entry is enabled if has_dual_part or (dual_entry_enabled and pos.get('status') == 'OPEN'): part = pos.get('dual_entry_part', 'first') # Default to 'first' if missing if part == 'first': # First entry - show countdown to Stage 2 trigger trigger_price = pos.get('dual_entry_trigger', 0) # Calculate trigger if missing if trigger_price == 0 and entry_price > 0: direction = pos.get('direction', 'LONG') if direction == 'LONG': trigger_price = entry_price * 1.005 else: trigger_price = entry_price * 0.995 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: