tion': h2_dir, 'h4_direction': h4_dir, 'detected': base_setup is not None and base_setup.get('detected', False) } # If we have a base setup, enhance it with confluence data if base_setup: base_entry = base_setup.get('entry', 0) base_sl = base_setup.get('stop_loss', 0) base_tp = base_setup.get('take_profit1', 0) base_dir = base_setup.get('direction', 'LONG') base_atr = base_setup.get('atr', 0) # CRITICAL FIX: If base_setup direction doesn't match final_direction, swap SL/TP if base_dir != final_direction and base_entry > 0: # Save original values BEFORE modifying orig_sl = base_sl orig_tp = base_tp # For LONG: SL < Entry < TP # For SHORT: TP < Entry < SL (or SL > Entry > TP) if final_direction == 'LONG': # Need SL below entry, TP above entry if base_sl > base_entry: # base was SHORT sl_distance = abs(orig_sl - base_entry) # Use SAVED original value tp_distance = abs(orig_tp - base_entry) # Use SAVED original value base_sl = base_entry - sl_distance base_tp = base_entry + tp_distance # Ensure minimum distances if base_sl >= base_entry: base_sl = base_entry * 0.985 # 1.5% below if base_tp <= base_entry: base_tp = base_entry * 1.03 # 3% above else: # final_direction == 'SHORT' # Need SL above entry, TP below entry if base_sl < base_entry: # base was LONG sl_distance = abs(orig_sl - base_entry) # Use SAVED original value tp_distance = abs(orig_tp - base_entry) # Use SAVED original value base_sl = base_entry + sl_distance base_tp = base_entry - tp_distance # Ensure minimum distances if base_sl <= base_entry: base_sl = base_entry * 1.015 # 1.5% above if base_tp >= base_entry: base_tp = base_entry * 0.97 # 3% below result.update({ 'pair': pair, 'score': base_setup.get('score', 0), 'grade': base_setup.get('grade', 'F'), 'price': base_setup.get('price', 0), 'entry': base_entry, 'stop_loss': round(base_sl, 2), 'take_profit1': round(base_tp, 2), 'rr_ratio': base_setup.get('rr_ratio', 0), 'rsi': base_setup.get('rsi', 0), 'signal_type': 'MULTI_TF_V2', 'factors': base_setup.get('factors', []), 'warnings': base_setup.get('warnings', []), 'vol_ratio': base_setup.get('vol_ratio', 1), 'exhaustion_score': exhaustion_score, # Use the multi_tf exhaustion_score 'exhaust_reversal_triggered': exhaust_reversal_triggered, 'original_direction': original_direction if exhaust_reversal_triggered else None, 'atr': base_atr, 'confluence_count': base_setup.get('confluence_count', 0), 'filter_warnings': base_setup.get('filter_warnings', None) }) # === FILTER ADX WARNINGS WHEN EXHAUST REVERSAL TRIGGERED === # If we're trading a reversal, ADX warnings against the reversal direction are expected # and should not be shown (they contradict the reversal thesis) if exhaust_reversal_triggered: filtered_warnings = [] for warning in result['warnings']: # Filter out ADX warnings that contradict the reversal direction if final_direction == 'LONG' and ('ADX bearish' in warning or 'long into strong trend' in warning): # Skip this warning - we're intentionally trading against the trend (reversal) result['factors'].append(f"✓ Ignored: {warning} (reversal trade)") continue elif final_direction == 'SHORT' and ('ADX bullish' in warning or 'short into strong trend' in warning): # Skip this warning - we're intentionally trading against the trend (reversal) result['factors'].append(f"✓ Ignored: {warning} (reversal trade)") continue filtered_warnings.append(warning) result['warnings'] = filtered_warnings # === END ADX WARNING FILTER === # Fix setup_type direction to match final_direction base_setup_type = base_setup.get('setup_type', 'MULTI_TF') if exhaust_reversal_triggered and original_direction: # Replace direction in setup_type base_setup_type = base_setup_type.replace(original_direction, final_direction) result['setup_type'] = base_setup_type else: result['setup_type'] = base_setup_type # Add confluence-specific factors if alignment['4h']: result['factors'].append(f"✓ H4 {h4_dir} trend (+40)") if alignment['2h']: result['factors'].append(f"✓ H2 {h2_dir} trend (+30)") if alignment['1h']: result['factors'].append(f"✓ H1 {h1_dir} trend (+30)") if alignment['15m']: result['factors'].append(f"✓ M15 {m15_dir} trigger (+30)") if alignment['5m']: result['factors'].append(f"✓ M5 {m5_dir} micro (+20)") if alignment['3m']: result['factors'].append(f"✓ M3 {m3_dir} micro (+20)") if all(alignment.values()): result['factors'].append("✓ All 6 timeframes aligned (+20 bonus)") # Add exhaust reversal factor if triggered if exhaust_reversal_triggered: if exhaustion_score <= 15: result['factors'].append(f"🔄 EXHAUST REVERSAL: Oversold ({exhaustion_score}) → LONG (+50)") elif exhaustion_score <= 30: result['factors'].append(f"🔄 EXHAUST REVERSAL: Oversold ({exhaustion_score}) → LONG (+25)") elif exhaustion_score >= 85: result['factors'].append(f"🔄 EXHAUST REVERSAL: Overbought ({exhaustion_score}) → SHORT (+50)") elif exhaustion_score >= 70: result['factors'].append(f"🔄 EXHAUST REVERSAL: Overbought ({exhaustion_score}) → SHORT (+25)") if h4_dir and m15_dir and m15_dir != h4_dir: result['warnings'].append(f"⚠ M15 ({m15_dir}) contradicts H4 ({h4_dir})") # === WEIGHTED SCORING INTEGRATION === weighted_data = calculate_timeframe_signals(tf_data, pair) result['weighted_score'] = weighted_data['weighted_score'] result['weighted_grade'] = weighted_data['confluence_grade'] result['higher_bias'] = weighted_data['higher_bias'] result['lower_bias'] = weighted_data['lower_bias'] result['reversal_detected'] = weighted_data['reversal_detected'] result['signal_values'] = weighted_data['signal_values'] # Add weighted factors result['factors'].append(f"Weighted Score: {weighted_data['weighted_score']:+.3f} (Grade {weighted_data['confluence_grade']})") if weighted_data['reversal_detected']: result['warnings'].append(f"🔄 REVERSAL: Lower TFs ({weighted_data['lower_bias']}) vs Higher TFs ({weighted_data['higher_bias']})") # === OVERRIDE DIRECTION WITH WEIGHTED SIGNAL === # If weighted signal strongly disagrees with timeframe alignment, use weighted # BUT: Respect exhaust reversal - if exhaustion triggered a reversal, don't override it weighted_direction = weighted_data['direction'] if weighted_direction != 'NEUTRAL' and final_direction != weighted_direction: # Check if weighted signal is strong enough to override weighted_score_abs = abs(weighted_data['weighted_score']) # DON'T override if exhaust reversal was triggered - exhaustion takes precedence if exhaust_reversal_triggered: result['factors'].append(f"✓ Keeping {final_direction} (exhaust reversal active, exhaust={exhaustion_score})") print(f"[EXHAUST PROTECTION] {pair}: Weighted says {weighted_direction} but exhaust reversal keeps {final_direction}") # IMPORTANT: Still need to update result direction! result['direction'] = final_direction elif weighted_score_abs > 0.35: # Strong signal threshold result['warnings'].append(f"⚠️ DIRECTION OVERRIDE: TF alignment says {final_direction}, but weighted signal ({weighted_data['weighted_score']:+.3f}) says {weighted_direction}") # Update direction old_direction = final_direction final_direction = weighted_direction result['direction'] = final_direction # Re-swap SL/TP for new direction if base_setup and base_entry > 0: if final_direction == 'LONG': if base_sl > base_entry: # Was SHORT base_sl = base_entry - abs(base_tp - base_entry) base_tp = base_entry + abs(base_sl - base_entry) * 1.5 if base_sl >= base_entry: base_sl = base_entry * 0.985 if base_tp <= base_entry: base_tp = base_entry * 1.03 else: # SHORT if base_sl < base_entry: # Was LONG base_sl = base_entry + abs(base_tp - base_entry) base_tp = base_entry - abs(base_sl - base_entry) * 1.5 if base_sl <= base_entry: base_sl = base_entry * 1.015 if base_tp >= base_entry: base_tp = base_entry * 0.97 result['stop_loss'] = round(base_sl, 2) result['take_profit1'] = round(base_tp, 2) # === MULTI-TIMEFRAME ADX ANALYSIS === # Calculate ADX across all timeframes for comprehensive trend analysis multi_tf_adx = calculate_multi_timeframe_adx(tf_data) result['multi_tf_adx'] = multi_tf_adx result['adx_weighted'] = multi_tf_adx['weighted_adx'] result['trend_consensus'] = multi_tf_adx['trend_consensus'] result['dominant_trend'] = multi_tf_adx['dominant_trend'] result['trend_shift_detected'] = multi_tf_adx['trend_shift_detected'] result['adx_recommendation'] = multi_tf_adx['recommendation'] # Add ADX-based factors if multi_tf_adx['trend_shift_detected']: result['factors'].append(f"⚡ Trend shift detected: {multi_tf_adx['lower_tf_bias']} vs {multi_tf_adx['higher_tf_bias']}") if multi_tf_adx['weighted_adx'] > 40: result['factors'].append(f"✓ Strong trend: ADX {multi_tf_adx['weighted_adx']:.1f}") elif multi_tf_adx['weighted_adx'] > 25: result['factors'].append(f"✓ Trending: ADX {multi_tf_adx['weighted_adx']:.1f}") if multi_tf_adx['trend_consensus'] > 80: result['factors'].append(f"✓ High TF consensus: {multi_tf_adx['trend_consensus']:.0f}%") # ADX-based warnings if multi_tf_adx['recommendation'] == 'NO_TRADE_RANGING': result['warnings'].append("⚠ Ranging market - low ADX across timeframes") elif multi_tf_adx['trend_shift_detected'] and result.get('direction'): if (multi_tf_adx['lower_tf_bias'] == 'LONG' and result['direction'] == 'SHORT') or \ (multi_tf_adx['lower_tf_bias'] == 'SHORT' and result['direction'] == 'LONG'): result['warnings'].append("⚠ Trade direction contradicts lower TF trend shift") # Print detailed log for log_line in weighted_data['detailed_log']: print(log_line) return result CORRELATION_MATRIX = { 'BTCUSDC': {'ETHUSDC': 0.85, 'SOLUSDC': 0.75, 'BNBUSDC': 0.70, 'XRPUSDC': 0.65, 'ADAUSDC': 0.70, 'DOGEUSDC': 0.60, 'TRXUSDC': 0.55, 'LINKUSDC': 0.72, 'AVAXUSDC': 0.78, 'SUIUSDC': 0.75}, 'ETHUSDC': {'BTCUSDC': 0.85, 'SOLUSDC': 0.80, 'BNBUSDC': 0.72, 'XRPUSDC': 0.68, 'ADAUSDC': 0.75, 'DOGEUSDC': 0.65, 'TRXUSDC': 0.60, 'LINKUSDC': 0.78, 'AVAXUSDC': 0.82, 'SUIUSDC': 0.80}, 'SOLUSDC': {'BTCUSDC': 0.75, 'ETHUSDC': 0.80, 'BNBUSDC': 0.68, 'XRPUSDC': 0.60, 'ADAUSDC': 0.70, 'DOGEUSDC': 0.62, 'TRXUSDC': 0.55, 'LINKUSDC': 0.72, 'AVAXUSDC': 0.85, 'SUIUSDC': 0.88}, 'BNBUSDC': {'BTCUSDC': 0.70, 'ETHUSDC': 0.72, 'SOLUSDC': 0.68, 'XRPUSDC': 0.58, 'ADAUSDC': 0.65, 'DOGEUSDC': 0.55, 'TRXUSDC': 0.52, 'LINKUSDC': 0.65, 'AVAXUSDC': 0.68, 'SUIUSDC': 0.70}, 'XRPUSDC': {'BTCUSDC': 0.65, 'ETHUSDC': 0.68, 'SOLUSDC': 0.60, 'BNBUSDC': 0.58, 'ADAUSDC': 0.72, 'DOGEUSDC': 0.58, 'TRXUSDC': 0.65, 'LINKUSDC': 0.60, 'AVAXUSDC': 0.62, 'SUIUSDC': 0.60}, 'ADAUSDC': {'BTCUSDC': 0.70, 'ETHUSDC': 0.75, 'SOLUSDC': 0.70, 'BNBUSDC': 0.65, 'XRPUSDC': 0.72, 'DOGEUSDC': 0.60, 'TRXUSDC': 0.58, 'LINKUSDC': 0.68, 'AVAXUSDC': 0.72, 'SUIUSDC': 0.70}, 'DOGEUSDC': {'BTCUSDC': 0.60, 'ETHUSDC': 0.65, 'SOLUSDC': 0.62, 'BNBUSDC': 0.55, 'XRPUSDC': 0.58, 'ADAUSDC': 0.60, 'TRXUSDC': 0.52, 'LINKUSDC': 0.58, 'AVAXUSDC': 0.60, 'SUIUSDC': 0.58}, 'TRXUSDC': {'BTCUSDC': 0.55, 'ETHUSDC': 0.60, 'SOLUSDC': 0.55, 'BNBUSDC': 0.52, 'XRPUSDC': 0.65, 'ADAUSDC': 0.58, 'DOGEUSDC': 0.52, 'LINKUSDC': 0.55, 'AVAXUSDC': 0.55, 'SUIUSDC': 0.53}, 'LINKUSDC': {'BTCUSDC': 0.72, 'ETHUSDC': 0.78, 'SOLUSDC': 0.72, 'BNBUSDC': 0.65, 'XRPUSDC': 0.60, 'ADAUSDC': 0.68, 'DOGEUSDC': 0.58, 'TRXUSDC': 0.55, 'AVAXUSDC': 0.75, 'SUIUSDC': 0.72}, 'AVAXUSDC': {'BTCUSDC': 0.78, 'ETHUSDC': 0.82, 'SOLUSDC': 0.85, 'BNBUSDC': 0.68, 'XRPUSDC': 0.62, 'ADAUSDC': 0.72, 'DOGEUSDC': 0.60, 'TRXUSDC': 0.55, 'LINKUSDC': 0.75, 'SUIUSDC': 0.83}, 'SUIUSDC': {'BTCUSDC': 0.75, 'ETHUSDC': 0.80, 'SOLUSDC': 0.88, 'BNBUSDC': 0.70, 'XRPUSDC': 0.60, 'ADAUSDC': 0.70, 'DOGEUSDC': 0.58, 'TRXUSDC': 0.53, 'LINKUSDC': 0.72, 'AVAXUSDC': 0.83}, } def get_pair_correlation(pair1, pair2): if pair1 == pair2: return 1.0 if pair1 in CORRELATION_MATRIX and pair2 in CORRELATION_MATRIX[pair1]: return CORRELATION_MATRIX[pair1][pair2] if pair2 in CORRELATION_MATRIX and pair1 in CORRELATION_MATRIX[pair2]: return CORRELATION_MATRIX[pair2][pair1] return 0.5 def calculate_portfolio_correlation(new_pair, active_pairs, threshold=0.75): if not active_pairs: return True, 0.0, None correlations = [get_pair_correlation(new_pair, active) for active in active_pairs] avg_corr = sum(correlations) / len(correlations) max_corr = max(correlations) if max_corr >= threshold: return False, avg_corr, f"HIGH CORRELATION: {new_pair} vs {active_pairs[correlations.index(max_corr)]} ({max_corr:.0%})" if avg_corr >= 0.70: return False, avg_corr, f"PORTFOLIO CORRELATED: Avg {avg_corr:.0%} with existing positions" return True, avg_corr, None def detect_market_regime(prices, volumes, lookback=20): if len(prices) < lookback: return {'regime': 'UNKNOWN', 'confidence': 0} highs = [max(prices[max(0, i-1):i+1]) for i in range(len(prices)-lookback, len(prices))] lows = [min(prices[max(0, i-1):i+1]) for i in range(len(prices)-lookback, len(prices))] plus_dm = [] minus_dm = [] for i in range(1, len(highs)): plus_dm.append(max(0, highs[i] - highs[i-1])) minus_dm.append(max(0, lows[i-1] - lows[i])) tr_list = [] for i in range(1, len(prices)): tr = max(prices[i] - prices[i-1], abs(prices[i] - prices[i-1]), abs(prices[i-1] - prices[i-1])) tr_list.append(tr) atr = sum(tr_list[-lookback:]) / lookback if tr_list else 0 upper, mid, lower = calculate_bollinger_bands(prices[-lookback:], period=20) bb_width = (upper - lower) / mid if mid > 0 else 0 current = prices[-1] range_high = max(prices[-lookback:]) range_low = min(prices[-lookback:]) range_size = range_high - range_low position_in_range = (current - range_low) / range_size if range_size > 0 else 0.5 sma_short = sum(prices[-5:]) / 5 sma_long = sum(prices[-lookback:]) / lookback trend_strength = abs(sma_short - sma_long) / sma_long * 100 if sma_long > 0 else 0 vol_recent = sum(volumes[-5:]) / 5 if len(volumes) >= 5 else 0 vol_old = sum(volumes[-lookback:-5]) / (lookback-5) if len(volumes) >= lookback else vol_recent vol_trend = vol_recent / vol_old if vol_old > 0 else 1.0 if trend_strength > 2.0 and bb_width > 0.05: if sma_short > sma_long: regime = 'TRENDING_UP' else: regime = 'TRENDING_DOWN' confidence = min(1.0, trend_strength / 5.0) elif bb_width < 0.03 or (range_size / current < 0.05): regime = 'RANGING' confidence = 0.8 elif trend_strength < 0.5: regime = 'CHOPPY' confidence = 0.7 else: regime = 'RANGING' confidence = 0.5 return { 'regime': regime, 'confidence': confidence, 'trend_strength': trend_strength, 'bb_width': bb_width, 'vol_trend': vol_trend, 'position_in_range': position_in_range } def should_trade_setup(setup, regime_info, min_regime_confidence=0.6, skip_regime_filter=False): if skip_regime_filter: return True, None regime = regime_info.get('regime', 'RANGING') confidence = regime_info.get('confidence', 0) direction = setup.get('direction', 'LONG') if confidence < min_regime_confidence: return False, f"Low regime confidence ({confidence:.0%})" if regime == 'CHOPPY': return False, "CHOPPY market - whipsaw risk" if regime == 'RANGING': setup_type = setup.get('setup_type', '') if 'RANGE_REVERSAL' not in setup_type and 'MOMENTUM_EXHAUSTION' not in setup_type: return False, "RANGING market - only reversal setups allowed" if regime == 'TRENDING_UP' and direction == 'SHORT': return False, "TRENDING_UP - no shorts" if regime == 'TRENDING_DOWN' and direction == 'LONG': return False, "TRENDING_DOWN - no longs" return True, None # ============================================================================ # SETUP QUEUE SYSTEM v1.0 - Producer-Consumer Architecture # ============================================================================ class SetupQueue: """ Thread-safe queue for managing trade setups. Producer (Scanner): Adds setups as soon as they're found Consumer (Bot/Modes): Fetches setups when needed Features: Age tracking, auto-refresh, stale cleanup """ def __init__(self, max_age_seconds=300, max_queue_size=50): self.queue = {} # pair -> setup dict with timestamp self.lock = threading.RLock() self.max_age_seconds = max_age_seconds # Default: 5 minutes self.max_queue_size = max_queue_size self._setup_callbacks = [] # Callbacks when new setup added self._last_access_time = time.time() self.stats = { 'added': 0, 'removed': 0, 'accessed': 0, 'refreshed': 0, 'expired': 0 } def add_setup(self, setup, source="scanner"): """Add or update a setup in the queue.""" if not setup or not setup.get('pair'): return False pair = setup['pair'] with self.lock: # Check if this is an update or new is_update = pair in self.queue # Enrich setup with metadata enriched_setup = setup.copy() enriched_setup.update({ 'timestamp_added': time.time(), 'timestamp_updated': time.time(), 'source': source, 'access_count': 0, 'age_check_count': 0 }) # If updating, preserve access count if is_update: enriched_setup['access_count'] = self.queue[pair].get('access_count', 0) enriched_setup['timestamp_added'] = self.queue[pair].get('timestamp_added', time.time()) self.queue[pair] = enriched_setup # Trim queue if too large (remove oldest) if len(self.queue) > self.max_queue_size: oldest_pair = min(self.queue.items(), key=lambda x: x[1]['timestamp_added'])[0] del self.queue[oldest_pair] self.stats['expired'] += 1 self.stats['added'] += 1 # Notify callbacks outside lock for callback in self._setup_callbacks: try: callback(enriched_setup, is_update) except Exception as e: print(f"[SETUP QUEUE] Callback error: {e}") action = "UPDATED" if is_update else "ADDED" print(f"[SETUP QUEUE] {action}: {pair} (Grade: {setup.get('grade', 'N/A')}, Queue: {len(self.queue)})") return True def get_setup(self, pair=None, min_grade=None, direction=None, max_age=None): """ Get a setup from the queue. If pair specified: returns that pair's setup or None If no pair: returns best matching setup based on filters """ with self.lock: self._last_access_time = time.time() self.stats['accessed'] += 1 if not self.queue: return None # Get specific pair if pair: if pair in self.queue: setup = self.queue[pair] if self._is_valid(setup, min_grade, direction, max_age): setup['access_count'] += 1 return setup.copy() return None # Find best setup matching criteria candidates = [] for p, setup in self.queue.items(): if self._is_valid(setup, min_grade, direction, max_age): # Score for ranking score = self._calculate_priority_score(setup) candidates.append((score, setup)) if not candidates: return None # Return highest scored setup candidates.sort(reverse=True, key=lambda x: x[0]) best_setup = candidates[0][1] best_setup['access_count'] += 1 return best_setup.copy() def get_all_setups(self, min_grade=None, direction=None, max_age=None, exclude_pairs=None): """Get all valid setups matching criteria.""" with self.lock: self._last_access_time = time.time() result = [] exclude_set = set(exclude_pairs or []) for pair, setup in self.queue.items(): if pair in exclude_set: continue if self._is_valid(setup, min_grade, direction, max_age): result.append(setup.copy()) # Sort by priority score result.sort(reverse=True, key=lambda s: self._calculate_priority_score(s)) return result def remove_setup(self, pair): """Remove a setup from the queue.""" with self.lock: if pair in self.queue: del self.queue[pair] self.stats['removed'] += 1 return True return False def refresh_setup(self, pair, new_setup): """Refresh a setup with new data (orderbook update, etc).""" if pair not in self.queue: return self.add_setup(new_setup, source="refresh") with self.lock: old_setup = self.queue[pair] # Merge new data with old merged = old_setup.copy() merged.update(new_setup) merged['timestamp_updated'] = time.time() merged['refresh_count'] = old_setup.get('refresh_count', 0) + 1 self.queue[pair] = merged self.stats['refreshed'] += 1 print(f"[SETUP QUEUE] REFRESHED: {pair}") return True def cleanup_expired(self, max_age_override=None): """Remove expired setups. Returns count removed.""" max_age = max_age_override or self.max_age_seconds now = time.time() to_remove = [] with self.lock: for pair, setup in self.queue.items(): age = now - setup.get('timestamp_updated', setup['timestamp_added']) if age > max_age: to_remove.append(pair) for pair in to_remove: del self.queue[pair] self.stats['expired'] += 1 if to_remove: print(f"[SETUP QUEUE] Cleaned {len(to_remove)} expired setups") return len(to_remove) def get_queue_status(self): """Get current queue status.""" with self.lock: now = time.time() setups_by_age = { 'fresh': 0, # < 60s 'recent': 0, # 60-180s 'aging': 0, # 180-300s 'stale': 0 # > 300s } for setup in self.queue.values(): age = now - setup.get('timestamp_updated', setup['timestamp_added']) if age < 60: setups_by_age['fresh'] += 1 elif age < 180: setups_by_age['recent'] += 1 elif age < 300: setups_by_age['aging'] += 1 else: setups_by_age['stale'] += 1 return { 'total': len(self.queue), 'by_age': setups_by_age, 'stats': self.stats.copy(), 'last_access': self._last_access_time } def _is_valid(self, setup, min_grade=None, direction=None, max_age=None): """Check if setup is valid (not expired, matches filters).""" now = time.time() age = now - setup.get('timestamp_updated', setup['timestamp_added']) # Check max age check_age = max_age or self.max_age_seconds if age > check_age: return False # Check grade if min_grade and setup.get('grade'): grade_order = ['F', 'D', 'C-', 'C', 'C+', 'B-', 'B', 'B+', 'A-', 'A', 'A+'] setup_idx = grade_order.index(setup['grade']) if setup['grade'] in grade_order else -1 min_idx = grade_order.index(min_grade) if min_grade in grade_order else -1 if setup_idx < min_idx: return False # Check direction if direction and setup.get('direction') != direction: return False return True def _calculate_priority_score(self, setup): """Calculate priority score for ranking setups.""" score = 0 # Base score from grade grade_scores = {'A+': 100, 'A': 95, 'A-': 90, 'B+': 85, 'B': 80, 'B-': 75, 'C+': 70, 'C': 65, 'D': 50, 'F': 30} score += grade_scores.get(setup.get('grade', 'F'), 30) # Bonus for freshness age = time.time() - setup.get('timestamp_updated', setup['timestamp_added']) if age < 30: score += 20 elif age < 60: score += 10 elif age < 120: score += 5 # Bonus for high access count (popular setups) score += min(10, setup.get('access_count', 0)) # Bonus for confluence score += min(20, setup.get('confluence_score', 0) / 10) return score def register_callback(self, callback): """Register callback for new setup notifications.""" self._setup_callbacks.append(callback) def unregister_callback(self, callback): """Unregister callback.""" if callback in self._setup_callbacks: self._setup_callbacks.remove(callback) # ============================================================================ # END SETUP QUEUE SYSTEM # ============================================================================ class PositionManager: def __init__(self, risk_per_trade=0.01, dxy_indicator=None, trade_logger=None, leverage=3, max_position_pct=0.20, target_rr=1.5): self.positions = [] self.daily_pnl = 0 self.weekly_pnl = 0 self.total_pnl = 0 # Robot Scanner default settings self.leverage = leverage # 3X default self.max_position_pct = max_position_pct # Max 20% per trade self.target_rr = target_rr # 1:2.5 R/R minimum at TP1 (optimal is 3.0) # TP/SL settings - 75% at TP1 (1.5 R/R min), 25% trails to TP2 self.tp1_pct = 0.75 # 75% at TP1 self.tp2_pct = 0.25 # 25% continues with trailing self.trail_pct = 0.005 # 0.5% trail for TP2 self.sl_buffer_pct = 0.001 self.risk_per_trade = risk_per_trade # Smart entry settings self.smart_entry_enabled = True self.move_sl_to_tp1 = True # Move SL close to TP1 after TP1 hit self.tp1_hit = False self.trail_start_pct = 0.10 # Start TP2 trailing 10% above TP1 self.sl_buffer_after_tp1 = 0.001 # SL buffer after TP1 hit self.trail_method = 'dynamic' # 'dynamic' or 'percent' self.dxy = dxy_indicator self.last_dxy_modifier = 1.0 self.last_dxy_reason = "" self.trade_logger = trade_logger self.pnl_history = self._load_pnl_history() self._last_update_time_ms = 0 # === FAILURE TRACKING: Cooldown system for failed entries === self.failed_entries = {} # pair -> {count, last_attempt_time} self.entry_cooldown_seconds = 300 # 5 minute cooldown after failure # === END FAILURE TRACKING === def calculate_position_size(self, available_balance, entry_price, stop_loss, direction, total_asset=None, use_dxy=True, market_params=None): """ Calculate position size with max 20% per trade rule. Market Brain can adjust size and leverage via market_params. Args: available_balance: Balance available for this specific trade entry_price: Entry price stop_loss: Stop loss price direction: 'LONG' or 'SHORT' total_asset: Total portfolio value (for max_position_pct calculation) use_dxy: Whether to use DXY modifier market_params: Dict with 'size_multiplier' and 'leverage' from Market Brain """ # Validate inputs if not entry_price or entry_price <= 0: print(f"[POSITION SIZE ERROR] Invalid entry_price: {entry_price}") return 0 if not stop_loss or stop_loss <= 0: print(f"[POSITION SIZE ERROR] Invalid stop_loss: {stop_loss}") return 0 if available_balance <= 0: print(f"[POSITION SIZE ERROR] Invalid available_balance: {available_balance}") return 0 # Use Market Brain leverage if provided, else default leverage = market_params.get('leverage', self.leverage) if market_params else self.leverage size_mult = market_params.get('size_multiplier', 1.0) if market_params else 1.0 # Calculate price risk percentage price_risk = abs(entry_price - stop_loss) / entry_price if price_risk == 0: return 0 # Risk-based position size (1% risk per trade - reward 1.5% target for 1:1.5 R/R) risk_amount = available_balance * self.risk_per_trade risk_based_size = risk_amount / price_risk * leverage # Max position value (20% of total asset with leverage) max_position_value = available_balance * self.max_position_pct * leverage # Use the smaller of the two position_size = min(max_position_value, risk_based_size) # Apply Market Brain size multiplier position_size = position_size * size_mult # === WEEKEND MODE: Reduce position size on weekends === try: from datetime import datetime now = datetime.now() is_weekend = now.weekday() >= 5 # Saturday=5, Sunday=6 if is_weekend and getattr(self, 'weekend_mode_enabled', True): weekend_mult = getattr(self, 'weekend_size_reduction', 0.5) position_size = position_size * weekend_mult leverage = leverage * getattr(self, 'weekend_leverage_reduction', 0.67) print(f"[POSITION SIZE] Weekend mode: {weekend_mult:.0%} size, {getattr(self, 'weekend_leverage_reduction', 0.67):.0%} leverage") except Exception: pass # Ignore weekend check errors # Log the calculation print(f"[POSITION SIZE] Available: ${available_balance:,.2f}") print(f"[POSITION SIZE] Price risk: {price_risk*100:.2f}%") print(f"[POSITION SIZE] Risk-based size: ${risk_based_size:,.2f}") print(f"[POSITION SIZE] Max position (20%): ${max_position_value:,.2f}") print(f"[POSITION SIZE] Market Brain mult: {size_mult:.2f}x") print(f"[POSITION SIZE] Leverage: {leverage}x") print(f"[POSITION SIZE] Final position: ${position_size:,.2f}") self.last_dxy_modifier = 1.0 self.last_dxy_reason = "No macro filter" if use_dxy and self.dxy: modifier, reason = self.dxy.get_position_modifier(direction) position_size = position_size * modifier self.last_dxy_modifier = modifier self.last_dxy_reason = reason quantity = position_size / entry_price return round(quantity, 6) def execute_dual_entry(self, setup, available_balance, paper=True, api_client=None, total_asset=None, market_params=None): """ Execute dual entry: 50% now, 50% on 0.5% favorable move. """ import time from datetime import datetime, timezone entry = setup.get('entry', 0) stop = setup.get('stop_loss', 0) tp = setup.get('take_profit1', 0) pair = setup.get('pair', '') direction = setup.get('direction', 'LONG') # Calculate position size (full size, but we'll split it) quantity = self.calculate_position_size(available_balance, entry, stop, direction, total_asset, market_params=market_params) if quantity <= 0: print(f"[DUAL ENTRY] Invalid quantity: {quantity}") return None # Split: 50% now, 50% later first_half_qty = quantity * 0.5 second_half_qty = quantity * 0.5 # Calculate trigger price for second entry (0.5% favorable move) if direction == 'LONG': trigger_price = entry * 1.005 else: # SHORT trigger_price = entry * 0.995 # Create modified setup for first entry first_setup = setup.copy() first_setup['quantity'] = first_half_qty first_setup['dual_entry_part'] = 'first' first_setup['dual_entry_trigger'] = trigger_price first_setup['dual_entry_qty'] = second_half_qty # Execute first entry using original logic position = self._open_position_original(first_setup, available_balance, paper, api_client, total_asset, market_params) if position: # Mark as dual entry position position['dual_entry'] = True position['tranche2_filled'] = False # Track dual entry state if not hasattr(self, 'dual_entry_pending'): self.dual_entry_pending = {} self.dual_entry_pending[pair] = { 'first_filled': True, 'entry_price': entry, 'direction': direction, 'remaining_size': second_half_qty, 'trigger_price': trigger_price, 'sl_price': stop, 'tp_price': tp, 'timestamp': time.time(), 'position_id': position.get('id') } print(f"[DUAL ENTRY] {pair}: First 50% filled at {entry}, trigger at {trigger_price}") return position def check_dual_entry_triggers(self, current_prices): """ Check if any dual entry triggers should fire. Call this regularly with current price data. """ if not hasattr(self, 'dual_entry_pending') or not self.dual_entry_pending: return for pair, state in list(self.dual_entry_pending.items()): if state['first_filled'] and not state.get('second_filled', False): current_price = current_prices.get(pair, 0) if current_price == 0: continue direction = state['direction'] trigger_price = state['trigger_price'] # Check if trigger hit trigger_hit = False if direction == 'LONG' and current_price >= trigger_price: trigger_hit = True elif direction == 'SHORT' and current_price <= trigger_price: trigger_hit = True if trigger_hit: print(f"[DUAL ENTRY] {pair}: Trigger hit! Current {current_price}, trigger {trigger_price}") # Find the original position and add to it for pos in self.positions: if pos['pair'] == pair and pos['status'] == 'OPEN': # Update position with second half pos['quantity'] += state['remaining_size'] pos['dual_entry_part'] = 'second' pos['tranche2_filled'] = True state['second_filled'] = True print(f"[DUAL ENTRY] {pair}: Second 50% filled. Total qty: {pos['quantity']}") break def open_position(self, setup, available_balance, paper=True, api_client=None, total_asset=None, market_params=None): """ Open a position. Supports dual entry mode (50% now, 50% on 0.5% favorable move). """ # Check if dual entry is enabled and this is a new entry (not a dual entry part) is_dual_entry_part = setup.get('dual_entry_part') is not None if getattr(self, 'dual_entry_enabled', False) and not is_dual_entry_part: # Use dual entry logic return self.execute_dual_entry(setup, available_balance, paper, api_client, total_asset, market_params) else: # Use normal entry return self._open_position_original(setup, available_balance, paper, api_client, total_asset, market_params) def _open_position_original(self, setup, available_balance, paper=True, api_client=None, total_asset=None, market_params=None): """Original open_position logic - preserved for dual entry internal calls.""" """Open a position with smart entry settings and Market Brain parameters.""" from datetime import datetime import time entry = setup.get('entry', 0) stop = setup.get('stop_loss', 0) tp = setup.get('take_profit1', 0) pair = setup.get('pair', '') direction = setup.get('direction', 'LONG') # === FAILURE COOLDOWN CHECK === current_time = time.time() if pair in self.failed_entries: last_fail_time = self.failed_entries[pair].get('last_attempt_time', 0) fail_count = self.failed_entries[pair].get('count', 0) cooldown = self.entry_cooldown_seconds * (1 + fail_count * 0.5) # Increase cooldown with failures time_since_fail = current_time - last_fail_time if time_since_fail < cooldown: print(f"[ENTRY COOLDOWN] {pair}: Blocked for {cooldown - time_since_fail:.0f}s ({fail_count} previous failures)") return None # === END FAILURE COOLDOWN === # === DEBUG LOGGING: Track entry attempts === fail_reason = None print(f"[OPEN_POSITION DEBUG] {pair} {direction}: Starting entry check...") print(f"[OPEN_POSITION DEBUG] Entry: ${entry}, SL: ${stop}, TP: ${tp}, Available: ${available_balance:,.2f}") # === END DEBUG LOGGING === # === STREAK ADAPTATION CHECK === if hasattr(self, 'streak_adapter'): adjustment = self.streak_adapter.get_adjustment() if not adjustment['should_trade']: fail_reason = f"Streak blocked: {adjustment['message']}" self.log_activity(f"[STREAK BLOCK] {pair}: {adjustment['message']}", "WARN") self._track_failed_entry(pair, fail_reason) return None elif adjustment['size_multiplier'] < 1.0: self.log_activity(f"[STREAK ADJUST] {pair}: {adjustment['message']}", "WARN") # Apply multiplier to available balance for calculation available_balance *= adjustment['size_multiplier'] # === END STREAK CHECK === # === SLIPPAGE-BASED SIZE ADJUSTMENT === if hasattr(self, 'slippage_monitor'): slippage_adjust = self.slippage_monitor.should_adjust_size(pair) if slippage_adjust['should_adjust']: self.log_activity(f"[SLIPPAGE ADJUST] {pair}: {slippage_adjust['reason']}", "WARN") available_balance *= slippage_adjust['multiplier'] # === END SLIPPAGE CHECK === # === SAFETY: Check position limit === open_count = len([p for p in self.positions if p['status'] == 'OPEN']) max_positions = getattr(self, 'max_positions', 3) # Default to 3 if open_count >= max_positions: fail_reason = f"Position limit reached: {open_count}/{max_positions}" print(f"[POSITION LIMIT] Cannot open {pair}: {open_count}/{max_positions} positions open") self._track_failed_entry(pair, fail_reason) return None # === END SAFETY CHECK === # === CORRELATION RISK CHECK === if hasattr(self, 'correlation_monitor') and open_count > 0: open_positions = [p for p in self.positions if p['status'] == 'OPEN'] corr_result = self.correlation_monitor.get_portfolio_correlation(open_positions, 'BTCUSDC') if corr_result['risk_level'] == 'high': fail_reason = f"High correlation risk: {corr_result.get('warning', 'Unknown')}" self.log_activity(f"[CORRELATION BLOCK] {pair}: {corr_result['warning']}", "WARN") self._track_failed_entry(pair, fail_reason) return None elif corr_result['risk_level'] == 'medium': self.log_activity(f"[CORRELATION] {pair}: {corr_result['warning']}", "WARN") # === END CORRELATION CHECK === # === VALIDATE SETUP DATA === if entry == 0 or stop == 0 or tp == 0 or not pair: fail_reason = f"Invalid setup data: entry={entry}, stop={stop}, tp={tp}, pair={pair}" print(f"[OPEN_POSITION FAIL] {pair}: {fail_reason}") self._track_failed_entry(pair, fail_reason) return None # === END VALIDATION === # Calculate position size with Market Brain parameters quantity = self.calculate_position_size(available_balance, entry, stop, direction, total_asset, market_params=market_params) if quantity <= 0: fail_reason = f"Invalid quantity calculated: {quantity} (insufficient balance or invalid SL)" print(f"[OPEN_POSITION FAIL] {pair}: {fail_reason}") self._track_failed_entry(pair, fail_reason) return None position_value = quantity * entry print(f"[OPEN_POSITION DEBUG] {pair}: Quantity={quantity:.6f}, Value=${position_value:,.2f}") # Check if in power hour (15:30-17:00) for enhanced R/R from datetime import datetime now = datetime.now() current_time = now.hour * 100 + now.minute # Format: 1530 for 15:30 is_power_hour = 1530 <= current_time <= 1700 # Dynamic R/R: Standard 1:1.5, Power hour 1:2.5, Optimal target 1:3 if is_power_hour: dynamic_rr = 2.5 # Power hour gets 1:2.5 minimum print(f"[POWER HOUR] {pair}: Using 1:2.5 R/R (15:30-17:00)") else: dynamic_rr = max(self.target_rr, 1.5) # Standard 1:1.5 minimum # Market Brain can override for optimal setups (target 1:3) if market_params: optimal_target = market_params.get('optimal_rr', 3.0) if optimal_target > dynamic_rr: dynamic_rr = optimal_target print(f"[MARKET BRAIN] {pair}: Optimal R/R override to 1:{dynamic_rr}") # Calculate TP levels - RESPECT setup's TP1 if provided (for scalping) setup_tp1 = setup.get('take_profit1', 0) setup_tp2 = setup.get('take_profit2', 0) if setup_tp1 > 0: # Use setup's TP values (for scalping mode with small targets) tp1 = setup_tp1 tp2 = setup_tp2 if setup_tp2 > 0 else tp1 * 1.02 if direction == "LONG" else tp1 * 0.98 print(f"[SCALP MODE] {pair}: Using setup TP1=${tp1:.4f} (not recalculated)") else: # Calculate TP levels based on dynamic R/R risk = abs(entry - stop) reward = risk * dynamic_rr if direction == "LONG": tp1 = entry + (reward * self.tp1_pct) tp2 = entry + reward else: tp1 = entry - (reward * self.tp1_pct) tp2 = entry - reward # === ADAPTIVE ENTRY OVERRIDE === # Check if adaptive entry provided better levels if setup.get('mode') == 'scalp' and setup.get('adaptive_stop_loss') and setup.get('adaptive_take_profit'): new_stop = setup['adaptive_stop_loss'] new_tp = setup['adaptive_take_profit'] if new_stop > 0 and new_tp > 0: print(f"[ADAPTIVE OVERRIDE] {pair}: Using adaptive levels for SCALP mode") print(f"[ADAPTIVE OVERRIDE] SL: ${stop:.4f} -> ${new_stop:.4f}") print(f"[ADAPTIVE OVERRIDE] TP1: ${tp1:.4f} -> ${new_tp:.4f}") stop = new_stop tp1 = new_tp # Also adjust TP2 for scalp mode tp2 = new_tp * 1.005 if direction == "LONG" else new_tp * 0.995 print(f"[ADAPTIVE OVERRIDE] TP2: ${tp2:.4f} (scalp extended)") elif setup.get('mode') == 'momentum' and setup.get('btc_aligned'): # For momentum mode with BTC alignment, use wider stops print(f"[ADAPTIVE OVERRIDE] {pair}: MOMENTUM mode with BTC alignment - using wider stops") if direction == "LONG": stop = min(stop, entry * 0.98) # Max 2% stop tp1 = max(tp1, entry * 1.02) # Min 2% target else: stop = max(stop, entry * 1.02) # Max 2% stop tp1 = min(tp1, entry * 0.98) # Min 2% target # === END ADAPTIVE OVERRIDE === # Validate TP2 is reasonable (not 100x entry or impossibly far!) max_reasonable_distance = entry * 0.15 # Max 15% move for TP2 tp2_distance = abs(tp2 - entry) if tp2 > entry * 10 or tp2 < entry * 0.1 or tp2_distance > max_reasonable_distance: print(f"[POSITION WARNING] {pair}: TP2 (${tp2:.4f}) is unrealistic! Entry=${entry:.4f}, Distance={tp2_distance/entry*100:.1f}%") # Recalculate TP2 as 1.5x the TP1 distance (more reasonable) tp1_distance = abs(tp1 - entry) if direction == "LONG": tp2 = entry + (tp1_distance * 1.5) else: tp2 = entry - (tp1_distance * 1.5) print(f"[POSITION CORRECTED] TP2 set to ${tp2:.4f} (1.5x TP1 distance)") # Validate TP1 is set correctly (not at/below entry for LONG, not at/above for SHORT) if direction == "LONG" and tp1 <= entry: print(f"[POSITION WARNING] {pair}: TP1 (${tp1:.4f}) <= Entry (${entry:.4f}) for LONG!") tp1 = entry * 1.01 # 1% above entry print(f"[POSITION WARNING] Auto-corrected TP1 to ${tp1:.4f}") elif direction == "SHORT" and tp1 >= entry: print(f"[POSITION WARNING] {pair}: TP1 (${tp1:.4f}) >= Entry (${entry:.4f}) for SHORT!") tp1 = entry * 0.99 # 1% below entry print(f"[POSITION WARNING] Auto-corrected TP1 to ${tp1:.4f}") # Validate SL is set correctly (not at/above entry for LONG, not at/below for SHORT) if direction == "LONG" and stop >= entry: print(f"[POSITION WARNING] {pair}: SL (${stop:.4f}) >= Entry (${entry:.4f}) for LONG!") stop = entry * 0.99 # 1% below entry print(f"[POSITION WARNING] Auto-corrected SL to ${stop:.4f}") elif direction == "SHORT" and stop <= entry: print(f"[POSITION WARNING] {pair}: SL (${stop:.4f}) <= Entry (${entry:.4f}) for SHORT!") stop = entry * 1.01 # 1% above entry print(f"[POSITION WARNING] Auto-corrected SL to ${stop:.4f}") # Validate TP1 != SL (they should be different!) if abs(tp1 - stop) < 0.0001: # Essentially equal print(f"[POSITION WARNING] {pair}: TP1 (${tp1:.4f}) == SL (${stop:.4f})! This is wrong.") if direction == "LONG": tp1 = entry * 1.01 # 1% above entry stop = entry * 0.99 # 1% below entry else: tp1 = entry * 0.99 # 1% below entry stop = entry * 1.01 # 1% above entry print(f"[POSITION WARNING] Fixed: TP1=${tp1:.4f}, SL=${stop:.4f}") position = { "id": str(uuid.uuid4())[:8], "pair": pair, "direction": direction, "entry_price": entry, "quantity": quantity, "position_value": position_value, "stop_loss": stop, "take_profit1": round(tp1, 2), # Use calculated tp1 "take_profit2": round(tp2, 2), "tp1_quantity": round(quantity * self.tp1_pct, 6), "tp2_quantity": round(quantity * self.tp2_pct, 6), "tp1_hit": False, "tp2_hit": False, "trailing_stop": None, "sl_at_breakeven": False, "trail_phase": 0, "tp1_hit_price": None, "tp2_hit_price": None, "trail_exit": False, "full_exit": False, "remaining_qty": round(quantity * self.tp2_pct, 6), "open_time": datetime.now(timezone.utc).isoformat(), "entry_time": time.time(), # Unix timestamp for time-based exits "partial_profit_taken": False, "profit_locked": False, "profit_locked_at": 0, "status": "OPEN", "paper": paper, "unrealized_pnl": 0, "orders": {"entry": None, "tp1": None, "tp2": None, "sl": None}, "entry_weighted_score": setup.get('weighted_score', 0), "entry_weighted_grade": setup.get('weighted_grade', 'F'), "entry_signal_values": setup.get('signal_values', {}), "reversal_check_count": 0, "last_reversal_check": None, "reversal_exits_blocked_until": None, "market_params": market_params if market_params else {}, "leverage_used": market_params.get('leverage', self.leverage) if market_params else self.leverage, "trade_pattern": market_params.get('trade_pattern', 'standard') if market_params else 'standard', "size_multiplier": market_params.get('size_multiplier', 1.0) if market_params else 1.0, "target_rr": dynamic_rr, "is_power_hour": is_power_hour, "dual_entry_part": setup.get('dual_entry_part'), "dual_entry_trigger": setup.get('dual_entry_trigger'), } if not paper and api_client and api_client.api_key: try: side = "BUY" if direction == "LONG" else "SELL" opposite_side = "SELL" if direction == "LONG" else "BUY" entry_order = api_client.place_order( symbol=pair, side=side, quantity=quantity, order_type='LIMIT', price=entry ) if entry_order: position['orders']['entry'] = entry_order.get('orderId') # === SLIPPAGE TRACKING === if hasattr(self, 'slippage_monitor') and entry_order.get('avgPrice'): actual_entry = float(entry_order['avgPrice']) slippage_result = self.slippage_monitor.record(pair, entry, actual_entry, direction) if slippage_result['is_excessive']: self.log_activity(f"[SLIPPAGE] {pair}: High slippage detected ({slippage_result['slippage_pct']:.2f}%)", "WARN") # === END SLIPPAGE TRACKING === tp1_order = api_client.place_order( symbol=pair, side=opposite_side, quantity=position['tp1_quantity'], order_type='LIMIT', price=tp, reduce_only=True ) if tp1_order: position['orders']['tp1'] = tp1_order.get('orderId') sl_order = api_client.place_order( symbol=pair, side=opposite_side, quantity=quantity, order_type='STOP_MARKET', stop_price=stop, reduce_only=True ) if sl_order: position['orders']['sl'] = sl_order.get('orderId') except Exception as e: print(f"Error placing orders: {e}") self.positions.append(position) # === CLEAR FAILED ENTRY TRACKING ON SUCCESS === if pair in self.failed_entries: del self.failed_entries[pair] print(f"[FAILED ENTRY CLEARED] {pair}: Entry successful, failure count reset") # === END CLEAR TRACKING === # === DEBUG: LOG SUCCESSFUL POSITION OPEN === print(f"[OPEN_POSITION SUCCESS] {pair}: Position opened successfully!") print(f"[OPEN_POSITION SUCCESS] ID: {position['id']}, Direction: {direction}") print(f"[OPEN_POSITION SUCCESS] Entry: ${entry:.4f}, SL: ${stop:.4f}, TP1: ${tp1:.4f}") print(f"[OPEN_POSITION SUCCESS] Quantity: {quantity:.6f}, Value: ${position_value:,.2f}") print(f"[OPEN_POSITION SUCCESS] Paper Mode: {paper}") # === END DEBUG LOGGING === # LOG POSITION OPENED try: from __main__ import get_behavior_logger logger = get_behavior_logger() if logger: logger.log_position_action(pair, "opened", { 'position_id': position['id'], 'direction': direction, 'entry': entry, 'size': position_value, 'leverage': position.get('leverage_used', 3), 'pattern': position.get('trade_pattern', 'standard'), 'rr_target': dynamic_rr, 'paper': paper }) except Exception as e: print(f"[BEHAVIOR LOG ERROR] {e}") if hasattr(self, 'sound_manager'): self.sound_manager.play('trade_entry') return position def update_position(self, pos_id, current_price, api_client=None): for pos in self.positions: if pos['id'] != pos_id or pos['status'] != 'OPEN': continue direction = pos['direction'] entry = pos['entry_price'] tp1 = pos['take_profit1'] tp2 = pos['take_profit2'] sl = pos['stop_loss'] qty = pos['quantity'] if direction == 'LONG': unrealized = (current_price - entry) / entry * pos['position_value'] else: unrealized = (entry - current_price) / entry * pos['position_value'] pos['unrealized_pnl'] = unrealized pos['current_price'] = current_price # === BOT-FATHER 2.0 HEAT MONITORING === if hasattr(self, 'botfather_v2'): try: # Calculate time in trade time_in_trade = time.time() - pos.get('entry_time', time.time()) # Get ATR for volatility klines_15m = self._get_cached_klines(pos['pair'], '15m', 20) atr_pct = 2.0 if klines_15m and len(klines_15m) >= 14: 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] atr = self._calculate_atr(highs, lows, closes, period=14) atr_pct = atr / current_price * 100 if current_price > 0 else 2.0 market_data = { 'unrealized_pct': unrealized / (entry * qty) * 100 if (entry * qty) > 0 else 0, 'time_in_trade': time_in_trade, 'price': current_price, 'atr_pct': atr_pct } heat_result = self.botfather_v2.calculate_heat(pos, market_data) if heat_result['heat_score'] >= 50: self.log_activity(f"[BOT-FATHER V2] {pos['pair']}: HEAT {heat_result['heat_score']}/100 ({heat_result['recommendation'].upper()})", "WARN") if heat_result['recommendation'] == 'close': self.log_activity(f"[BOT-FATHER V2] {pos['pair']}: EMERGENCY CLOSE recommended", "ERROR") except Exception as e: print(f"[BOTFATHER V2 ERROR] {e}") # === END HEAT MONITORING === # Update trade logger with price extremes for MFE/MAE tracking if self.trade_logger: self.trade_logger.update_price_extremes(pos_id, current_price) if not pos['tp1_hit']: # Validate TP1 is in correct direction before checking hit tp1_valid = True if direction == 'LONG' and tp1 <= entry: tp1_valid = False print(f"[TP1 WARNING] {pos['pair']}: TP1 (${tp1:.2f}) <= Entry (${entry:.2f}), skipping hit check") elif direction == 'SHORT' and tp1 >= entry: tp1_valid = False print(f"[TP1 WARNING] {pos['pair']}: TP1 (${tp1:.2f}) >= Entry (${entry:.2f}), skipping hit check") if tp1_valid and ((direction == 'LONG' and current_price >= tp1) or \ (direction == 'SHORT' and current_price <= tp1)): pos['tp1_hit'] = True # Move SL close to TP1 when TP1 is hit (protect 1.5 R/R profit on remaining 50%) # SL is placed slightly below TP1 for LONG, slightly above for SHORT if direction == 'LONG': pos['stop_loss'] = tp1 * 0.999 # 0.1% below TP1 else: pos['stop_loss'] = tp1 * 1.001 # 0.1% above TP1 pos['sl_at_breakeven'] = False # SL is now at profit level (close to TP1) # Start TP2 trailing from slightly ABOVE TP1 (not from entry) # This gives room for the price to breathe but protects profit if direction == 'LONG': # Initial trail starts 10% above TP1 toward TP2 trail_start = tp1 + (tp2 - tp1) * 0.10 pos['trailing_stop'] = trail_start else: trail_start = tp1 - (tp1 - tp2) * 0.10 pos['trailing_stop'] = trail_start # Track TP1 hit price for later calculations pos['tp1_hit_price'] = current_price pos['trail_phase'] = 1 # Phase 1: Between TP1 and TP2 pos['tp2_trailing'] = False # TP2 trailing not yet active if not pos.get('paper', True) and api_client and pos['orders'].get('sl'): try: opposite_side = "SELL" if direction == "LONG" else "BUY" api_client.cancel_order(pos['pair'], pos['orders']['sl']) except Exception as e: print(f"Error updating SL: {e}") return 'TP1_HIT', pos['tp1_quantity'] if pos['tp1_hit'] and not pos['tp2_hit']: # PHASE 1: Between TP1 and TP2 - wait for price to move above TP1 + 10% to start trailing # Check if we should activate TP2 trailing (10% above TP1 toward TP2) if direction == 'LONG': trail_activation = tp1 + (tp2 - tp1) * 0.10 # 10% above TP1 if current_price >= trail_activation and not pos.get('tp2_trailing', False): pos['tp2_trailing'] = True print(f"[TP2 TRAIL] {pos['pair']}: TP2 trailing activated at ${current_price:.2f}") else: # SHORT trail_activation = tp1 - (tp1 - tp2) * 0.10 if current_price <= trail_activation and not pos.get('tp2_trailing', False): pos['tp2_trailing'] = True print(f"[TP2 TRAIL] {pos['pair']}: TP2 trailing activated at ${current_price:.2f}") # If TP2 trailing is active, apply trailing logic based on method if pos.get('tp2_trailing', False): trail_method = getattr(self, 'trail_method', 'dynamic') if trail_method == 'dynamic': # DYNAMIC TRAILING: Uses ATR-based volatility adjustment # Calculate profit in R multiples if direction == 'LONG': r_multiple = (current_price - entry) / (entry - sl) if entry != sl else 1 else: r_multiple = (entry - current_price) / (sl - entry) if sl != entry else 1 # Adjust trail distance based on R multiple (profit level) if r_multiple >= 3.0: trail_pct = 0.003 # Very tight trail at 3R+ elif r_multiple >= 2.0: trail_pct = 0.005 # Standard tight trail at 2R+ else: trail_pct = 0.008 # Wider trail between 1.5R and 2R else: # PERCENT TRAILING: Fixed percentage trail trail_pct = self.trail_pct # User-defined fixed trail (default 0.5%) if direction == 'LONG': new_trail = current_price * (1 - trail_pct) # Never trail below TP1 (protect the 1.5 R/R) min_trail = tp1 * 0.998 if new_trail > pos.get('trailing_stop', 0) and new_trail >= min_trail: pos['trailing_stop'] = new_trail print(f"[TP2 TRAIL {trail_method.upper()}] {pos['pair']}: Trail ({trail_pct*100:.2f}%) updated to ${new_trail:.2f}") else: # SHORT new_trail = current_price * (1 + trail_pct) max_trail = tp1 * 1.002 if new_trail < pos.get('trailing_stop', float('inf')) and new_trail <= max_trail: pos['trailing_stop'] = new_trail print(f"[TP2 TRAIL {trail_method.upper()}] {pos['pair']}: Trail ({trail_pct*100:.2f}%) updated to ${new_trail:.2f}") # Check if hit trailing stop if (direction == 'LONG' and current_price <= pos['trailing_stop']) or \ (direction == 'SHORT' and current_price >= pos['trailing_stop']): pos['tp2_hit'] = True pos['trail_exit'] = True print(f"[TP2 TRAIL] {pos['pair']}: Hit {trail_method} trailing stop at ${pos['trailing_stop']:.2f}") return 'TRAILING_STOP', pos['tp2_quantity'] # Check if hit hard TP2 if (direction == 'LONG' and current_price >= tp2) or \ (direction == 'SHORT' and current_price <= tp2): pos['tp2_hit'] = True pos['tp2_hit_price'] = current_price pos['trail_phase'] = 2 # Phase 2: After TP2 hit print(f"[TP2 HIT] {pos['pair']}: Hard TP2 hit at ${current_price:.2f}") return 'TP2_HIT', pos['tp2_quantity'] # Check if hit SL (now set close to TP1 for protection) if (direction == 'LONG' and current_price <= sl) or \ (direction == 'SHORT' and current_price >= sl): pos['tp2_hit'] = True pos['sl_exit'] = True print(f"[SL HIT AFTER TP1] {pos['pair']}: SL hit at ${sl:.2f} (close to TP1)") return 'SL_AFTER_TP1', pos['tp2_quantity'] # Check initial SL (before TP1 hit) if not pos['tp1_hit']: if (direction == 'LONG' and current_price <= sl) or \ (direction == 'SHORT' and current_price >= sl): return 'SL_HIT', qty return None, 0 return None, 0 def close_position(self, pos_id, exit_price, reason="MANUAL", closed_qty=None): """Close a position and log the trade""" print(f"[CLOSE POSITION] Attempting to close {pos_id} at ${exit_price:.2f}, reason={reason}") for pos in self.positions: if pos['id'] == pos_id and pos['status'] == 'OPEN': entry = pos['entry_price'] qty = closed_qty or pos['quantity'] size = qty * entry if pos['direction'] == 'LONG': pnl = (exit_price - entry) / entry * size else: pnl = (entry - exit_price) / entry * size print(f"[CLOSE POSITION] {pos['pair']} {pos['direction']}: Entry=${entry:.2f}, Exit=${exit_price:.2f}, PnL=${pnl:.2f}") if 'realized_pnl' not in pos: pos['realized_pnl'] = 0 pos['closed_quantity'] = 0 pos['realized_pnl'] += pnl pos['closed_quantity'] += qty if pos['closed_quantity'] >= pos['quantity']: pos['status'] = 'CLOSED' pos['exit_price'] = exit_price pos['exit_time'] = datetime.now(timezone.utc).isoformat() pos['exit_reason'] = reason # === STREAK TRACKING === if hasattr(self, 'streak_adapter'): outcome = 'win' if pnl > 0 else 'loss' self.streak_adapter.record(outcome, pnl) streak_info = self.streak_adapter.get_consecutive() if streak_info['count'] > 1: self.log_activity(f"[STREAK] {outcome.upper()} streak: {streak_info['count']} consecutive", "SUCCESS" if outcome == 'win' else "WARN") # === END STREAK TRACKING === # LOG POSITION CLOSED try: from __main__ import get_behavior_logger logger = get_behavior_logger() if logger: pnl_pct = (pnl / (entry * qty)) * 100 if qty > 0 else 0 logger.log_position_action(pos['pair'], "closed", { 'position_id': pos_id, 'exit_price': exit_price, 'pnl': pnl, 'pnl_pct': pnl_pct, 'reason': reason, 'duration_min': (datetime.now(timezone.utc) - datetime.fromisoformat(pos['open_time'].replace('Z', '+00:00'))).total_seconds() / 60 }) except Exception as e: print(f"[BEHAVIOR LOG ERROR] {e}") # Log trade exit if self.trade_logger: pnl_pct = (pnl / (entry * qty)) * 100 if qty > 0 else 0 print(f"[CLOSE POSITION] Logging trade exit: {pos_id}, PnL%={pnl_pct:.2f}%") self.trade_logger.log_trade_exit(pos_id, exit_price, reason, pnl_pct) print(f"[CLOSE POSITION] Trade logged successfully") else: print(f"[CLOSE POSITION] WARNING: No trade_logger available!") if hasattr(self, 'sound_manager'): self.sound_manager.play('trade_exit') self.total_pnl += pnl self.daily_pnl += pnl self.weekly_pnl += pnl # DEBUG: Log PnL calculation print(f"[PNL DEBUG] Closed {pos['pair']}: PnL=${pnl:+.2f}, Entry=${entry:.2f}, Exit=${exit_price:.2f}, Qty={qty:.4f}") print(f"[PNL DEBUG] New totals: daily=${self.daily_pnl:+.2f}, weekly=${self.weekly_pnl:+.2f}, total=${self.total_pnl:+.2f}") self._save_pnl_history() return pnl print(f"[CLOSE POSITION] Position {pos_id} not found or not open") return 0 def close_all_positions(self, reason="MANUAL_CLOSE_ALL"): count = 0 total_pnl = 0 for pos in self.positions: if pos['status'] == 'OPEN': current = pos.get('current_price', pos['entry_price']) pnl = self.close_position(pos['id'], current, reason) total_pnl += pnl count += 1 return count, total_pnl def get_stats(self): closed = [p for p in self.positions if p['status'] == 'CLOSED'] wins = [p for p in closed if p.get('realized_pnl', 0) > 0] unrealized = sum(p.get('unrealized_pnl', 0) for p in self.positions if p['status'] == 'OPEN') open_count = len([p for p in self.positions if p['status'] == 'OPEN']) # === AGGRESSIVE PNL CORRUPTION DETECTION === # Check for impossible PnL values max_reasonable_daily = 5000 # $5k max daily PnL (was $1000) max_reasonable_weekly = 25000 # $25k max weekly (was $5000) max_reasonable_total = 100000 # $100k max total (was $100000) pnl_corrupted = False corruption_reason = [] if abs(self.daily_pnl) > max_reasonable_daily: corruption_reason.append(f"daily ${self.daily_pnl:.2f} > ${max_reasonable_daily}") pnl_corrupted = True if abs(self.weekly_pnl) > max_reasonable_weekly: corruption_reason.append(f"weekly ${self.weekly_pnl:.2f} > ${max_reasonable_weekly}") pnl_corrupted = True if abs(self.total_pnl) > max_reasonable_total: corruption_reason.append(f"total ${self.total_pnl:.2f} > ${max_reasonable_total}") pnl_corrupted = True # Check if total position value justifies PnL total_position_value = sum(p.get('position_value', 0) for p in self.positions if p['status'] == 'OPEN') if total_position_value > 0 and abs(self.daily_pnl) > total_position_value * 5: # PnL > 5x position value corruption_reason.append(f"daily PnL ${self.daily_pnl:.2f} > 5x position value ${total_position_value * 5:.2f}") pnl_corrupted = True # Auto-reset if corrupted if pnl_corrupted: print(f"[PNL CORRUPTION DETECTED] {', '.join(corruption_reason)} - AUTO-RESETTING!") self.daily_pnl = 0 self.weekly_pnl = 0 self.total_pnl = 0 self._save_pnl_history() return { "total_trades": len(closed), "win_rate": round(len(wins) / len(closed) * 100, 1) if closed else 0, "total_pnl": round(self.total_pnl, 2), "daily_pnl": round(self.daily_pnl, 2), "weekly_pnl": round(self.weekly_pnl, 2), "open_positions": open_count, "unrealized_pnl": round(unrealized, 2) } def get_available_balance(self, total_balance): used = sum(p['position_value'] for p in self.positions if p['status'] == 'OPEN') return total_balance - (used / self.leverage) def reset_pnl(self): """Reset P&L tracking with timestamp logging""" now = datetime.now(timezone.utc).isoformat() self.daily_pnl = 0 self.weekly_pnl = 0 self.total_pnl = 0 # Track reset times self._last_daily_reset = now self._last_weekly_reset = now for pos in self.positions: if pos['status'] == 'CLOSED': pos['realized_pnl'] = 0 self._save_pnl_history() print(f"[PNL] Reset at {now}") return True def _load_pnl_history(self): """Load P&L history with auto-reset for daily/weekly periods""" try: if os.path.exists(PNL_LOG_FILE): with open(PNL_LOG_FILE, 'r') as f: data = json.load(f) # Check if we need to reset daily/weekly based on last update last_updated_str = data.get('last_updated') if last_updated_str: last_updated = datetime.fromisoformat(last_updated_str) now = datetime.now(timezone.utc) # Reset daily if it's a new day if last_updated.date() != now.date(): print(f"[PNL] New day detected - resetting daily P&L") self.daily_pnl = 0 else: self.daily_pnl = data.get('daily_pnl', 0) # Reset weekly if it's been 7 days days_since_update = (now.date() - last_updated.date()).days if days_since_update >= 7: print(f"[PNL] Weekly reset - {days_since_update} days since last update") self.weekly_pnl = 0 else: self.weekly_pnl = data.get('weekly_pnl', 0) else: # No last_updated, load all values self.daily_pnl = data.get('daily_pnl', 0) self.weekly_pnl = data.get('weekly_pnl', 0) # Total P&L never resets (it's cumulative) self.total_pnl = data.get('total_pnl', 0) # Store last reset dates for display self._last_daily_reset = data.get('last_daily_reset', last_updated_str or now.isoformat()) self._last_weekly_reset = data.get('last_weekly_reset', last_updated_str or now.isoformat()) # VALIDATION: Reset if values are suspiciously large (likely corrupted) # Check daily - reasonable daily PnL should be under $1000 for normal trading if abs(self.daily_pnl) > 1000: print(f"[PNL] WARNING: Daily PnL ${self.daily_pnl:+.2f} seems corrupted. Resetting to 0.") self.daily_pnl = 0 # Check weekly - reasonable weekly PnL should be under $5000 if abs(self.weekly_pnl) > 5000: print(f"[PNL] WARNING: Weekly PnL ${self.weekly_pnl:+.2f} seems corrupted. Resetting to 0.") self.weekly_pnl = 0 # Check total - if total is absurdly high, reset all if abs(self.total_pnl) > 100000: print(f"[PNL] WARNING: Total PnL ${self.total_pnl:+.2f} seems corrupted. Resetting all PnL.") self.daily_pnl = 0 self.weekly_pnl = 0 self.total_pnl = 0 print(f"[PNL] Loaded: Daily=${self.daily_pnl:+.2f}, Weekly=${self.weekly_pnl:+.2f}, Total=${self.total_pnl:+.2f}") return data.get('history', []) except Exception as e: print(f"[PNL] Error loading history: {e}") return [] def _save_pnl_history(self): """Save P&L history with reset tracking""" try: now = datetime.now(timezone.utc).isoformat() data = { 'daily_pnl': self.daily_pnl, 'weekly_pnl': self.weekly_pnl, 'total_pnl': self.total_pnl, 'last_updated': now, 'last_daily_reset': getattr(self, '_last_daily_reset', now), 'last_weekly_reset': getattr(self, '_last_weekly_reset', now), 'history': self.pnl_history[-100:] } with open(PNL_LOG_FILE, 'w') as f: json.dump(data, f, indent=2) except Exception as e: print(f"[PNL] Error saving history: {e}") def log_pnl_snapshot(self, open_positions_count=0): snapshot = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'daily_pnl': round(self.daily_pnl, 2), 'weekly_pnl': round(self.weekly_pnl, 2), 'total_pnl': round(self.total_pnl, 2), 'open_positions': open_positions_count } self.pnl_history.append(snapshot) self._save_pnl_history() return snapshot def get_pnl_report(self, days=7): from datetime import datetime, timedelta cutoff = datetime.now(timezone.utc) - timedelta(days=days) recent = [h for h in self.pnl_history if datetime.fromisoformat(h['timestamp']) > cutoff] return { 'period': f'Last {days} days', 'entries': len(recent), 'start_pnl': recent[0]['total_pnl'] if recent else 0, 'end_pnl': self.total_pnl, 'change': round(self.total_pnl - (recent[0]['total_pnl'] if recent else 0), 2), 'history': recent } def _track_failed_entry(self, pair, reason): """Track failed entry attempts with cooldown management.""" import time current_time = time.time() if pair not in self.failed_entries: self.failed_entries[pair] = {'count': 0, 'last_attempt_time': 0, 'reasons': []} self.failed_entries[pair]['count'] += 1 self.failed_entries[pair]['last_attempt_time'] = current_time self.failed_entries[pair]['reasons'].append({'time': current_time, 'reason': reason}) # Keep only last 5 reasons self.failed_entries[pair]['reasons'] = self.failed_entries[pair]['reasons'][-5:] print(f"[FAILED ENTRY TRACKED] {pair}: {reason} (Total failures: {self.failed_entries[pair]['count']})") def get_failed_entry_report(self): """Get report of recent failed entry attempts.""" import time current_time = time.time() report = [] for pair, data in self.failed_entries.items(): time_since = current_time - data['last_attempt_time'] cooldown_remaining = max(0, self.entry_cooldown_seconds - time_since) report.append({ 'pair': pair, 'fail_count': data['count'], 'last_fail_ago': f"{time_since:.0f}s", 'cooldown_remaining': f"{cooldown_remaining:.0f}s", 'latest_reason': data['reasons'][-1]['reason'] if data['reasons'] else 'Unknown' }) return report # ============================================================================ # MAGIC ENTRY SYSTEM v1.0 - Advanced Split Order & Dynamic Sizing # ============================================================================ class MagicEntryEngine: """Advanced entry with split orders, dynamic sizing, smart loss cutting""" def __init__(self, app_reference): self.app = app_reference self.active_entries = {} def calculate_split_sizes(self, total_size, confidence_score, grade='B'): """Split entry: initial/confirmation/momentum""" grade_boost = {'A+': 1.2, 'A': 1.15, 'A-': 1.1, 'B+': 1.05, 'B': 1.0, 'B-': 0.95}.get(grade, 0.9) adjusted_conf = min(100, confidence_score * grade_boost) if adjusted_conf >= 80: return [(0.60, 'initial'), (0.25, 'confirmation'), (0.15, 'momentum')] elif adjusted_conf >= 65: return [(0.50, 'initial'), (0.30, 'confirmation'), (0.20, 'momentum')] else: return [(0.40, 'initial'), (0.35, 'confirmation'), (0.25, 'momentum')] def calculate_dynamic_size(self, base_size, setup, confirmation_signals): """Dynamic sizing with logarithmic scaling""" size = base_size grade = setup.get('grade', 'F') grade_mult = {'A+': 1.5, 'A': 1.3, 'A-': 1.2, 'B+': 1.1, 'B': 1.0, 'B-': 0.9, 'C+': 0.8, 'C': 0.7, 'C-': 0.6, 'D+': 0.5, 'D': 0.4, 'D-': 0.3, 'F': 0.2}.get(grade, 0.5) size *= grade_mult # Confirmation boosts if confirmation_signals.get('momentum_aligned'): size *= 1.15 if confirmation_signals.get('volume_confirmed'): size *= 1.10 if confirmation_signals.get('structure_intact'): size *= 1.10 if confirmation_signals.get('rsi_favorable'): size *= 1.05 # Volatility adjustment volatility = setup.get('atr_14_pct', 0.02) vol_factor = max(0.5, min(1.5, 1.0 - (volatility - 0.02) * 10)) size *= vol_factor return min(size, base_size * 2.0) def should_add_to_position(self, position, current_price, signals): """Add to winning position?""" entry = position.get('entry_price', 0) if entry == 0: return False, 0, "" direction = position.get('direction', 'LONG') if direction == 'LONG': move_pct = (current_price - entry) / entry if move_pct >= 0.005 and signals.get('momentum_continues'): return True, position.get('position_value', 0) * 0.25, f"+{move_pct*100:.2f}%" else: move_pct = (entry - current_price) / entry if move_pct >= 0.005 and signals.get('momentum_continues'): return True, position.get('position_value', 0) * 0.25, f"-{move_pct*100:.2f}%" return False, 0, "" def should_cut_loss(self, position, current_price, max_loss_pct=1.5): """Smart loss cutting""" entry = position.get('entry_price', 0) direction = position.get('direction', 'LONG') if entry == 0: return False, "", False if direction == 'LONG': pnl_pct = (current_price - entry) / entry * 100 else: pnl_pct = (entry - current_price) / entry * 100 if pnl_pct <= -max_loss_pct: return True, f"Max loss {pnl_pct:.2f}%", True if pnl_pct < -0.8 and position.get('structure_broken'): return True, f"Structure break {pnl_pct:.2f}%", True return False, f"Holding {pnl_pct:+.2f}%", False def log(self, message, level="INFO"): if self.app: self.app.log_activity(f"[MAGIC] {message}", level) # ============================================================================ class StyledButton(Button): def __init__(self, text, bg_color, text_color, font_size=sp(12), radius=12, **kwargs): super().__init__(text=text, color=text_color, font_size=font_size, background_color=(0,0,0,0), background_normal='', background_down='', **kwargs) self._radius = dp(radius) self.bg_color = bg_color self.outline_color = BLACK self.canvas.before.clear() with self.canvas.before: Color(*self.bg_color) self.rect = RoundedRectangle(pos=self.pos, size=self.size, radius=[self._radius]*4) self.bind(pos=self._update_graphics, size=self._update_graphics) Clock.schedule_once(self._update_graphics, 0) def _update_graphics(self, *args): self.rect.pos = self.pos self.rect.size = self.size def set_bg_color(self, color): self.bg_color = color self.canvas.before.clear() with self.canvas.before: Color(*self.bg_color) self.rect = RoundedRectangle(pos=self.pos, size=self.size, radius=[self._radius]*4) class RoundedTextInput(TextInput): def __init__(self, radius=8, **kwargs): kwargs.setdefault('background_color', (0.15, 0.15, 0.15, 1)) kwargs.setdefault('foreground_color', (1, 1, 1, 1)) kwargs.setdefault('cursor_color', (1, 1, 1, 1)) kwargs.setdefault('hint_text_color', (0.5, 0.5, 0.5, 1)) kwargs.setdefault('multiline', False) kwargs.setdefault('font_size', sp(14)) kwargs.setdefault('padding', [dp(12), dp(12), dp(12), dp(12)]) super().__init__(**kwargs) # GLOBAL TEXTINPUT STYLING from kivy.uix.textinput import TextInput from kivy.factory import Factory # Set default TextInput properties class BorderedCard(BoxLayout): def __init__(self, **kwargs): super().__init__(**kwargs) # GLOBAL TEXTINPUT STYLING from kivy.uix.textinput import TextInput from kivy.factory import Factory # Set default TextInput properties self.padding = dp(6) # Reduced from dp(12) self.spacing = dp(4) # Reduced from dp(8) with self.canvas.before: Color(*CARD_BG) self.rect = RoundedRectangle(pos=self.pos, size=self.size, radius=[dp(16)]*4) self.bind(pos=self._update_graphics, size=self._update_graphics) def _update_graphics(self, *args): self.rect.pos = self.pos self.rect.size = self.size # ============================================================================ # MACRO & MARKET BRAIN CARDS # ============================================================================ class MacroIndicatorCard(BorderedCard): """UI Card displaying DXY and Oil prices""" def __init__(self, **kwargs): super().__init__(**kwargs) # GLOBAL TEXTINPUT STYLING from kivy.uix.textinput import TextInput from kivy.factory import Factory # Set default TextInput properties self.orientation = 'vertical' self.size_hint_y = None self.height = dp(90) self.padding = dp(8) self.spacing = dp(2) self.fetcher = MacroDataFetcher() # Title self.add_widget(Label( text='[b]🌍 MACRO[/b]', markup=True, color=GOLD, font_size=sp(11), size_hint_y=0.25 )) # DXY Row dxy_box = BoxLayout(size_hint_y=0.25) self.dxy_lbl = Label(text='DXY: --', color=WHITE, font_size=sp(10), halign='left') self.dxy_lbl.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) self.dxy_chg = Label(text='(--%)', color=GRAY, font_size=sp(10), halign='right') self.dxy_chg.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) dxy_box.add_widget(self.dxy_lbl) dxy_box.add_widget(self.dxy_chg) self.add_widget(dxy_box) # Oil Row oil_box = BoxLayout(size_hint_y=0.25) self.oil_lbl = Label(text='OIL: $--', color=WHITE, font_size=sp(10), halign='left') self.oil_lbl.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) self.oil_chg = Label(text='(--%)', color=GRAY, font_size=sp(10), halign='right') self.oil_chg.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) oil_box.add_widget(self.oil_lbl) oil_box.add_widget(self.oil_chg) self.add_widget(oil_box) # Bias Row self.bias_lbl = Label(text='Bias: --', color=AMBER, font_size=sp(9), size_hint_y=0.25, halign='center') self.bias_lbl.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) self.add_widget(self.bias_lbl) Clock.schedule_interval(self.update, 300) self.update() def update(self, dt=None): try: # Check for dual entry triggers (every minute) if hasattr(self, 'check_dual_entry_triggers'): self.check_dual_entry_triggers() dxy = self.fetcher.get_dxy() oil = self.fetcher.get_crude_oil() bias, color = self.fetcher.get_bias() self.dxy_lbl.text = f"DXY: {dxy['price']:.1f}" self.dxy_chg.text = f"({dxy['change_pct']:+.1f}%)" self.dxy_chg.color = GREEN if dxy['change'] >= 0 else RED self.oil_lbl.text = f"OIL: ${oil['price']:.1f}" self.oil_chg.text = f"({oil['change_pct']:+.1f}%)" self.oil_chg.color = GREEN if oil['change'] >= 0 else RED self.bias_lbl.text = f"Bias: {bias}" self.bias_lbl.color = color except Exception as e: print(f"[MACRO CARD] {e}") # ============================================================================ # END MACRO INDICATORS class MarketBrainCard(BorderedCard): """24/7 Market Brain status card""" def __init__(self, brain, **kwargs): super().__init__(**kwargs) # GLOBAL TEXTINPUT STYLING from kivy.uix.textinput import TextInput from kivy.factory import Factory # Set default TextInput properties self.brain = brain self.orientation = 'vertical' self.size_hint_y = None self.height = dp(140) self.padding = dp(8) self.spacing = dp(2) # Title self.add_widget(Label( text='[b]BRAIN: MARKET PRO[/b]', markup=True, color=GOLD, font_size=sp(11), size_hint_y=0.18 )) # Market Type self.type_lbl = Label(text='Type: --', color=WHITE, font_size=sp(10), size_hint_y=0.16) self.add_widget(self.type_lbl) # Strategy self.strat_lbl = Label(text='Strategy: --', color=CYAN, font_size=sp(9), size_hint_y=0.16) self.add_widget(self.strat_lbl) # Bias & Risk row bias_box = BoxLayout(size_hint_y=0.16) self.bias_lbl = Label(text='Bias: --', color=AMBER, font_size=sp(9), size_hint_x=0.5) self.risk_lbl = Label(text='Risk: --', color=AMBER, font_size=sp(9), size_hint_x=0.5) bias_box.add_widget(self.bias_lbl) bias_box.add_widget(self.risk_lbl) self.add_widget(bias_box) # Confidence self.conf_lbl = Label(text='Conf: --', color=GRAY, font_size=sp(9), size_hint_y=0.16) self.add_widget(self.conf_lbl) # Position multiplier self.mult_lbl = Label(text='Size: 1.00x', color=GREEN, font_size=sp(9), size_hint_y=0.18) self.add_widget(self.mult_lbl) Clock.schedule_interval(self.update, 60) # Update every minute self.update() def update(self, dt=None): try: # Use cached values if available if hasattr(self.brain, 'market_type'): type_colors = { 'bull': GREEN, 'bear': RED, 'sideways': AMBER, 'volatile': PURPLE } mtype = self.brain.market_type.value if hasattr(self.brain.market_type, 'value') else str(self.brain.market_type) self.type_lbl.text = f"Type: {mtype.upper()}" self.type_lbl.color = type_colors.get(mtype, WHITE) if hasattr(self.brain, 'primary_strategy'): strat = self.brain.primary_strategy.value if hasattr(self.brain.primary_strategy, 'value') else str(self.brain.primary_strategy) self.strat_lbl.text = f"Strategy: {strat}" bias_colors = {'BULLISH': GREEN, 'BEARISH': RED, 'NEUTRAL': AMBER} self.bias_lbl.text = f"Bias: {self.brain.market_bias}" self.bias_lbl.color = bias_colors.get(self.brain.market_bias, AMBER) risk_colors = {'RISK_ON': GREEN, 'RISK_OFF': RED, 'NEUTRAL': AMBER} self.risk_lbl.text = f"Risk: {self.brain.risk_mode}" self.risk_lbl.color = risk_colors.get(self.brain.risk_mode, AMBER) if hasattr(self.brain, 'confidence'): self.conf_lbl.text = f"Conf: {self.brain.confidence:.0%}" params = self.brain.get_position_params(base_size=1.0) self.mult_lbl.text = f"Size: {params['size_multiplier']:.2f}x | Lev: {params['leverage']}x" except Exception as e: print(f"[BRAIN CARD] {e}") #!/usr/bin/env python3 """ MARKET BRAIN PRO v2.0 - 24/7 PROFIT ENGINE =========================================== All-weather trading intelligence for bull, bear, and sideways markets. """ import requests import statistics from datetime import datetime, timedelta from enum import Enum try: from rob_bot_domination import integrate_unified_brain DOMINATION_AVAILABLE = True except: DOMINATION_AVAILABLE = False class SetupCard(BorderedCard): def __init__(self, setup_data, on_trade_callback=None, **kwargs): super().__init__(**kwargs) # GLOBAL TEXTINPUT STYLING from kivy.uix.textinput import TextInput from kivy.factory import Factory # Set default TextInput properties self.orientation = 'vertical' self.setup_data = setup_data self.size_hint_y = None self.padding = (dp(16), dp(12), dp(16), dp(12)) # Increased side padding self.spacing = dp(8) # Increased spacing between elements direction = setup_data.get('direction', 'LONG') dir_color = GREEN if direction == 'LONG' else RED setup_type = setup_data.get('setup_type', 'STANDARD') signal_type = setup_data.get('signal_type', 'THOR') grade = setup_data.get('grade', 'B-') warnings = setup_data.get('warnings', []) # Check if this is a multi-timeframe setup confluence_score = setup_data.get('confluence_score', 0) is_multi_tf = confluence_score > 0 or signal_type == 'MULTI_TF_V2' tf_alignment = setup_data.get('timeframe_alignment', {}) # Get pair first (needed for debug and display) pair = setup_data.get('pair', 'BTCUSDC') # DEBUG: Log setup card creation details print(f"[SETUP CARD DEBUG] {pair}: confluence_score={confluence_score}, signal_type={signal_type}, is_multi_tf={is_multi_tf}, tf_alignment={tf_alignment}") # Check if this is a scalp setup is_scalp = setup_data.get('is_scalp', False) scalp_confidence = setup_data.get('scalp_confidence', 0) # Calculate warning height contribution warnings = setup_data.get('warnings', []) warn_extra_height = dp(0) if warnings: warn_text = "⚠ " + " | ".join(warnings[:2]) warn_lines = max(1, len(warn_text) // 35) warn_extra_height = dp(20 + warn_lines * 18) # Calculate factors height contribution factors = setup_data.get('factors', []) factors_extra_height = dp(0) if factors: factors_text = " | ".join(factors[:3]) if factors else "" factors_lines = max(1, len(factors_text) // 40) factors_extra_height = dp(16 + factors_lines * 14) # Adjust height for different setup types - DYNAMIC based on content if is_scalp: base_height = dp(400) if is_multi_tf else dp(360) elif is_multi_tf: base_height = dp(360) else: base_height = dp(320) # Add dynamic content height self.height = base_height + warn_extra_height + factors_extra_height grade_colors = { 'A+': (0.0, 0.9, 0.3, 1), 'A': (0.2, 0.85, 0.2, 1), 'A-': (0.3, 0.8, 0.2, 1), 'B+': (0.4, 0.75, 0.2, 1), 'B': (0.9, 0.8, 0.1, 1), 'B-': (0.95, 0.7, 0.1, 1), } grade_color = grade_colors.get(grade, GOLD) entry = setup_data.get('entry', 0) tp = setup_data.get('take_profit1', 0) sl = setup_data.get('stop_loss', 0) rr = setup_data.get('rr_ratio', 0) factors = setup_data.get('factors', []) # Use current_price/price/market_price - fallback to entry if not available market_price = setup_data.get('current_price', setup_data.get('price', setup_data.get('market_price', entry))) # Calculate % to TP from current market price (not entry) pct_to_tp = 0 if market_price > 0 and tp > 0: if direction == 'LONG': pct_to_tp = ((tp - market_price) / market_price) * 100 else: pct_to_tp = ((market_price - tp) / market_price) * 100 # Format prices with correct decimals entry_str = format_price(entry, pair) tp_str = format_price(tp, pair) market_str = format_price(market_price, pair) # Header with Pair + Market Price + Direction + Grade header = BoxLayout(size_hint_y=0.12, spacing=dp(4)) # Pair (bold) + Market Price (white) - Market price after pair # format_price already adds $, so don't add another pair_market_text = f"[b]{pair}[/b] [color=ffffff]{market_str}[/color]" header.add_widget(Label( text=pair_market_text, markup=True, color=dir_color, font_size=sp(13), size_hint_x=0.50 )) # Direction + R/R dir_rr_text = f"[b]{direction}[/b] [color=aaaaaa]R/R 1:{rr:.1f}[/color]" header.add_widget(Label( text=dir_rr_text, markup=True, color=dir_color, font_size=sp(11), size_hint_x=0.30 )) # Grade header.add_widget(Label( text=f"[b]{grade}[/b]", markup=True, color=grade_color, font_size=sp(13), size_hint_x=0.20 )) self.add_widget(header) # Add timeframe alignment row for multi-timeframe setups if is_multi_tf: # Add "Timeframe Direction Indicators" header tf_header_row = BoxLayout(size_hint_y=0.08, spacing=dp(2), padding=(0, dp(2))) tf_header_lbl = Label( text="[b]Timeframe Direction Indicators[/b]", markup=True, color=GREEN, font_size=sp(11), halign='center', valign='middle' ) tf_header_row.add_widget(tf_header_lbl) self.add_widget(tf_header_row) tf_row = BoxLayout(size_hint_y=0.08, spacing=dp(2)) # All 6 timeframes: 3m, 5m, 15m, 1h, 2h, 4h m3_aligned = tf_alignment.get('3m', False) m5_aligned = tf_alignment.get('5m', False) m15_aligned = tf_alignment.get('15m', False) h1_aligned = tf_alignment.get('1h', False) h2_aligned = tf_alignment.get('2h', False) h4_aligned = tf_alignment.get('4h', False) m3_color = GREEN if m3_aligned else GRAY m5_color = GREEN if m5_aligned else GRAY m15_color = GREEN if m15_aligned else GRAY h1_color = GREEN if h1_aligned else GRAY h2_color = GREEN if h2_aligned else GRAY h4_color = GREEN if h4_aligned else GRAY # Convert tuples to hex for markup color tags def rgb_to_hex(rgb): return f"{int(rgb[0]*255):02x}{int(rgb[1]*255):02x}{int(rgb[2]*255):02x}" m3_color_hex = rgb_to_hex(m3_color) m5_color_hex = rgb_to_hex(m5_color) m15_color_hex = rgb_to_hex(m15_color) h1_color_hex = rgb_to_hex(h1_color) h2_color_hex = rgb_to_hex(h2_color) h4_color_hex = rgb_to_hex(h4_color) m3_dir = setup_data.get('m3_direction', '') m5_dir = setup_data.get('m5_direction', '') m15_dir = setup_data.get('m15_direction', '') h1_dir = setup_data.get('h1_direction', '') h2_dir = setup_data.get('h2_direction', '') h4_dir = setup_data.get('h4_direction', '') # Compact display: 3m 5m 15m H1 H2 H4 tf_text = ( f"[color={m3_color_hex}][b]{'✓' if m3_aligned else '○'}M3[/b][/color] " f"[color={m5_color_hex}][b]{'✓' if m5_aligned else '○'}M5[/b][/color] " f"[color={m15_color_hex}][b]{'✓' if m15_aligned else '○'}M15{m15_dir[:1] if m15_dir else ''}[/b][/color] " f"[color={h1_color_hex}][b]{'✓' if h1_aligned else '○'}H1{h1_dir[:1] if h1_dir else ''}[/b][/color] " f"[color={h2_color_hex}][b]{'✓' if h2_aligned else '○'}H2{h2_dir[:1] if h2_dir else ''}[/b][/color] " f"[color={h4_color_hex}][b]{'✓' if h4_aligned else '○'}H4{h4_dir[:1] if h4_dir else ''}[/b][/color]" ) tf_row.add_widget(Label( text=tf_text, markup=True, color=WHITE, font_size=sp(9) )) self.add_widget(tf_row) type_row = BoxLayout(size_hint_y=0.08, padding=(8, 2)) type_display = setup_type.replace('_', ' ') # Add exhaust reversal indicator if triggered exhaust_reversal = setup_data.get('exhaust_reversal_triggered', False) if exhaust_reversal: orig_dir = setup_data.get('original_direction', '') exhaust_score = setup_data.get('exhaustion_score', 50) if exhaust_score <= 15: reversal_text = f" [color=00ff00][b]↻ REVERSAL (OVERSOLD)[/b][/color]" elif exhaust_score >= 85: reversal_text = f" [color=ff0000][b]↻ REVERSAL (OVERBOUGHT)[/b][/color]" else: reversal_text = f" [color=ffff00][b]↻ REVERSAL[/b][/color]" type_display += reversal_text type_lbl = Label( text=f"[b]{type_display}[/b] | RSI: {setup_data.get('rsi', 0)} | Vol: {setup_data.get('vol_ratio', 1)}x", markup=True, color=AMBER, font_size=sp(11), halign='center', valign='middle' ) type_lbl.bind(width=lambda inst, w: setattr(inst, 'text_size', (w - dp(16), None))) type_row.add_widget(type_lbl) self.add_widget(type_row) if warnings: # Calculate approximate height based on warning text length warn_text = "⚠ " + " | ".join(warnings[:2]) # Estimate: ~40 chars per line, ~24px per line estimated_lines = max(1, len(warn_text) // 35) warn_row_height = dp(20 + estimated_lines * 18) warn_row = BoxLayout(size_hint_y=None, height=warn_row_height, padding=(16, 6)) warn_lbl = Label( text=f"[b]{warn_text}[/b]", markup=True, color=(1, 0.4, 0.4, 1), font_size=sp(9), halign='center', valign='middle', size_hint_x=1.0, text_size=(None, None) # Will be set by binding ) # Bind to parent's width minus padding for proper wrapping def update_text_size(inst, w): # Account for padding [dp(16), dp(6)] on both sides inst.text_size = (w - dp(32), None) warn_lbl.bind(width=update_text_size) warn_row.add_widget(warn_lbl) self.add_widget(warn_row) details = GridLayout(cols=2, size_hint_y=0.50, spacing=dp(8), padding=(12, 4)) label_size = sp(13) value_size = sp(13) lbl_market = Label(text="[b]Market:[/b]", markup=True, color=WHITE, font_size=label_size, halign='left', valign='middle', size_hint_x=0.35) lbl_market.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) details.add_widget(lbl_market) val_market = Label(text=f"[b]{format_price(market_price, pair)}[/b]", markup=True, color=WHITE, font_size=value_size, halign='right', valign='middle', size_hint_x=0.65) val_market.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) details.add_widget(val_market) # Entry and TP1 now shown in header - only show SL here lbl_sl = Label(text="[b]Stop:[/b]", markup=True, color=GRAY, font_size=label_size, halign='left', valign='middle') lbl_sl.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) details.add_widget(lbl_sl) val_sl = Label(text=f"[b]{format_price(sl, pair)}[/b]", markup=True, color=RED, font_size=value_size, halign='right', valign='middle') val_sl.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) details.add_widget(val_sl) lbl_rr = Label(text="R/R:", color=GRAY, font_size=label_size, halign='left', valign='middle') lbl_rr.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) details.add_widget(lbl_rr) rr_color = GREEN if rr >= 2 else (AMBER if rr >= 1.5 else GRAY) val_rr = Label(text=f"[b]1:{rr}[/b]", markup=True, color=rr_color, font_size=value_size, halign='right', valign='middle') val_rr.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) details.add_widget(val_rr) atr = setup_data.get('atr', 0) if atr > 0: lbl_atr = Label(text="ATR:", color=GRAY, font_size=label_size, halign='left', valign='middle') lbl_atr.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) details.add_widget(lbl_atr) val_atr = Label(text=f"[b]{format_price(atr, pair)}[/b]", markup=True, color=CYAN, font_size=value_size, halign='right', valign='middle') val_atr.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) details.add_widget(val_atr) exhaustion = setup_data.get('exhaustion_score', 0) if exhaustion > 0: lbl_exh = Label(text="Exhaust:", color=GRAY, font_size=label_size, halign='left', valign='middle') lbl_exh.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) details.add_widget(lbl_exh) exh_text, exh_color, direction_arrow = self.get_exhaustion_state(exhaustion, setup_data.get('exhaustion_trend', 'neutral')) val_exh = Label( text=f"[b]{exhaustion} {direction_arrow}[/b]\n[color=aaaaaa]{exh_text}[/color]", markup=True, color=exh_color, font_size=sp(11), halign='right', valign='middle' ) val_exh.bind(width=lambda inst, w: setattr(inst, 'text_size', (w, None))) details.add_widget(val_exh) self.add_widget(details) factors_text = " | ".join(factors[:3]) if factors else "No signals" # Calculate dynamic height based on text length factors_lines = max(1, len(factors_text) // 40) factors_height = dp(16 + factors_lines * 14) factors_lbl = Label( text=factors_text, color=GRAY, font_size=sp(10), halign='center', size_hint_y=None, height=factors_height, padding=(8, 2) ) factors_lbl.bind(width=lambda inst, w: setattr(inst, 'text_size', (w - dp(16), None))) self.add_widget(factors_lbl) # === ADVICED ACTION ROW === # Determine the advised action based on setup quality action_row = BoxLayout(size_hint_y=0.08, spacing=dp(4)) # Determine action and color if warnings and len(warnings) > 0: # Has warnings - recommend WAIT action_text = "WAIT" action_color = (0.9, 0.9, 0.9, 1) # Light gray action_bg = (0.4, 0.4, 0.4, 1) # Gray background elif grade in ['A+', 'A', 'A-']: # High grade - strong entry signal if direction == 'LONG': action_text = "ENTER LONG" action_color = GREEN action_bg = (0.1, 0.4, 0.1, 1) else: action_text = "ENTER SHORT" action_color = RED action_bg = (0.4, 0.1, 0.1, 1) elif grade in ['B+', 'B', 'B-']: # Medium grade - cautious entry if direction == 'LONG': action_text = "ENTER LONG (Cautious)" action_color = (0.6, 0.9, 0.6, 1) # Light green action_bg = (0.15, 0.35, 0.15, 1) else: action_text = "ENTER SHORT (Cautious)" action_color = (0.9, 0.6, 0.6, 1) # Light red action_bg = (0.35, 0.15, 0.15, 1) else: # Low grade - wait for better setup action_text = "WAIT (Low Grade)" action_color = AMBER action_bg = (0.4, 0.3, 0.1, 1) # Override for ranging market multi_tf_adx = setup_data.get('multi_tf_adx', {}) if multi_tf_adx.get('recommendation') == 'NO_TRADE_RANGING': action_text = "WAIT (Ranging Market)" action_color = AMBER action_bg = (0.4, 0.3, 0.1, 1) # Override for trend shift detected if multi_tf_adx.get('trend_shift_detected') and not exhaust_reversal: action_text = "WAIT (Trend Shift)" action_color = AMBER action_bg = (0.4, 0.3, 0.1, 1) action_label = Label( text=f"[b]ADVICE: {action_text}[/b]", markup=True, color=action_color, font_size=sp(12), halign='center' ) action_row.add_widget(action_label) self.add_widget(action_row) # Add SCALP indicator row if this is a scalp setup if is_scalp: scalp_row = BoxLayout(size_hint_y=0.08, spacing=dp(4)) scalp_label = Label( text=f"[b]⚡ SCALP {scalp_confidence}% ⚡[/b]", markup=True, color=(1, 0.9, 0, 1), # Bright yellow font_size=sp(14) ) scalp_row.add_widget(scalp_label) self.add_widget(scalp_row) # Start blinking animation Clock.schedule_interval(lambda dt: self._blink_scalp_label(scalp_label), 0.5) # Extra spacing row before buttons self.add_widget(BoxLayout(size_hint_y=0.05)) btn_row = BoxLayout(size_hint_y=0.16, spacing=dp(8), padding=(8, 2)) if on_trade_callback: # If scalp setup, add SCALP button if is_scalp: scalp_btn = StyledButton( text="[b]⚡ SCALP NOW[/b]", markup=True, bg_color=(1, 0.85, 0, 1), # Yellow/gold text_color=(0, 0, 0, 1), # Black text for contrast font_size=sp(12), radius=12, size_hint_x=0.5 ) scalp_btn.bind(on_press=lambda x: self._execute_scalp_trade(setup_data, on_trade_callback)) btn_row.add_widget(scalp_btn) trade_btn = StyledButton( text="[b]TRADE NOW[/b]", markup=True, bg_color=dir_color, text_color=WHITE, font_size=sp(12), radius=12, size_hint_x=0.70 if is_scalp else 0.75 ) trade_btn.bind(on_press=lambda x: self._confirm_trade(setup_data, on_trade_callback)) btn_row.add_widget(trade_btn) disqualify_btn = StyledButton( text="[b]✕[/b]", markup=True, bg_color=(0.5, 0.1, 0.1, 1), text_color=WHITE, font_size=sp(10), radius=8, size_hint_x=0.15 if is_scalp else 0.15 ) disqualify_btn.bind(on_press=lambda x: self._confirm_disqualify(setup_data)) btn_row.add_widget(disqualify_btn) self.add_widget(btn_row) self.app_ref = None self._popup = None def _blink_scalp_label(self, label): """Blink the SCALP label between yellow and orange.""" current_color = label.color if current_color[0] > 0.95 and current_color[1] > 0.85: # Yellow label.color = (1, 0.5, 0, 1) # Orange else: label.color = (1, 0.9, 0, 1) # Yellow return True # Keep scheduling def _execute_scalp_trade(self, setup_data, on_trade_callback): """Execute scalp trade immediately without confirmation.""" pair = setup_data.get('pair', 'Unknown') direction = setup_data.get('direction', 'LONG') confidence = setup_data.get('scalp_confidence', 0) print(f"[SCALP_EXEC] Executing scalp trade for {pair} {direction} ({confidence}% confidence)") # Mark as scalp trade setup_data['is_scalp_trade'] = True setup_data['scalp_entry_time'] = time.time() # Execute immediately on_trade_callback(setup_data) # Log the scalp if hasattr(self, 'app_ref') and self.app_ref: self.app_ref.log_activity(f"[b]⚡ SCALP EXECUTED:[/b] {pair} {direction} @ {confidence}% confidence", "TRADE") def _confirm_trade(self, setup_data, on_trade_callback): pair = setup_data.get('pair', 'Unknown') direction = setup_data.get('direction', 'LONG') entry = setup_data.get('entry', 0) sl = setup_data.get('stop_loss', 0) tp = setup_data.get('take_profit1', 0) grade = setup_data.get('grade', 'F') rr = setup_data.get('rr_ratio', 0) content = BoxLayout(orientation='vertical', padding=dp(15), spacing=dp(10)) content.add_widget(Label( text=f"[b]CONFIRM TRADE?[/b]\n\n{pair} {direction}\nGrade: {grade} | R/R: 1:{rr:.1f}\nEntry: ${entry:.4f}\nSL: ${sl:.4f} | TP: ${tp:.4f}", markup=True, color=WHITE, font_size=sp(14), halign='center' )) btn_row = BoxLayout(size_hint_y=None, height=dp(50), spacing=dp(10)) cancel_btn = StyledButton(text="[b][NO] CANCEL[/b]", markup=True, bg_color=GRAY, text_color=WHITE, font_size=sp(11), radius=8) confirm_btn = StyledButton(text="[b][OK] EXECUTE[/b]", markup=True, bg_color=GREEN if direction == 'LONG' else RED, text_color=WHITE, font_size=sp(11), radius=8) self._popup = Popup(title='', content=content, size_hint=(0.85, 0.4), background_color=(0.1, 0.1, 0.12, 1), separator_color=(0, 0, 0, 0)) cancel_btn.bind(on_press=self._popup.dismiss) confirm_btn.bind(on_press=lambda x: self._execute_trade(setup_data, on_trade_callback)) btn_row.add_widget(cancel_btn) btn_row.add_widget(confirm_btn) content.add_widget(btn_row) self._popup.open() def _execute_trade(self, setup_data, on_trade_callback): if self._popup: self._popup.dismiss() on_trade_callback(setup_data) def _confirm_disqualify(self, setup_data): pair = setup_data.get('pair', 'Unknown') direction = setup_data.get('direction', 'LONG') content = BoxLayout(orientation='vertical', padding=dp(15), spacing=dp(10)) content.add_widget(Label( text=f"[b]DISQUALIFY SETUP?[/b]\n\n{pair} {direction}\n\nRemove from setups?", markup=True, color=WHITE, font_size=sp(14), halign='center' )) btn_row = BoxLayout(size_hint_y=None, height=dp(50), spacing=dp(10)) cancel_btn = StyledButton(text="[b]KEEP[/b]", markup=True, bg_color=GRAY, text_color=WHITE, font_size=sp(11), radius=8) confirm_btn = StyledButton(text="[b]DISQUALIFY[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(11), radius=8) self._popup = Popup(title='', content=content, size_hint=(0.85, 0.4), background_color=(0.1, 0.1, 0.12, 1), separator_color=(0, 0, 0, 0)) cancel_btn.bind(on_press=self._popup.dismiss) confirm_btn.bind(on_press=lambda x: self._execute_disqualify(setup_data)) btn_row.add_widget(cancel_btn) btn_row.add_widget(confirm_btn) content.add_widget(btn_row) self._popup.open() def _execute_disqualify(self, setup_data): if self._popup: self._popup.dismiss() self.disqualify_setup(setup_data) def disqualify_setup(self, setup_data): pair = setup_data.get('pair', 'Unknown') if not self.app_ref: from kivy.app import App self.app_ref = App.get_running_app() if self.app_ref and hasattr(self.app_ref, 'root'): app_root = self.app_ref.root if hasattr(app_root, 'current_setups'): app_root.current_setups = [ s for s in app_root.current_setups if s.get('pair') != pair or s.get('direction') != setup_data.get('direction') ] if not hasattr(app_root, 'disqualified_pairs'): app_root.disqualified_pairs = [] app_root.disqualified_pairs.append({ 'pair': pair, 'direction': setup_data.get('direction'), 'grade': setup_data.get('grade'), 'timestamp': datetime.now().isoformat() }) app_root.log_activity(f"[b]DISQUALIFIED:[/b] {pair} {setup_data.get('direction')} - Removed from setups", "WARN") app_root.refresh_setups_display() def get_exhaustion_state(self, score, trend): up_arrow = "UP" down_arrow = "DN" if 1 <= score <= 18: if trend == 'up': return "Oversold BOUNCING", GREEN, up_arrow else: return "Heavily OVERSOLD", GREEN, "" elif 19 <= score <= 25: if trend == 'up': return "Oversold moving UP", GREEN, up_arrow elif trend == 'down': return "Oversold moving DN", AMBER, down_arrow else: return "Oversold zone", GREEN, "" elif 26 <= score <= 40: if trend == 'up': return "Bullish momentum UP", GREEN, up_arrow elif trend == 'down': return "Bullish fading DN", AMBER, down_arrow else: return "Bullish build", WHITE, "" elif 41 <= score <= 59: return "Neutral zone", GRAY, "" elif 60 <= score <= 74: if trend == 'up': return "Bearish building UP", AMBER, up_arrow elif trend == 'down': return "Bearish momentum DN", RED, down_arrow else: return "Bearish pressure", ORANGE, "" elif 75 <= score <= 85: if trend == 'down': return "Overbought DROPPING", RED, down_arrow else: return "Heavily OVERBOUGHT", RED, "" elif 86 <= score <= 100: if trend == 'down': return "Extreme SELLING", RED, down_arrow else: return "EXTREME overbought", RED, "" else: return "Invalid", GRAY, "" INFO_TEXTS = { "min_grade": "Minimum grade filter", "min_rr": "Minimum risk/reward ratio", "time_limits": "Time restrictions", "circuit_breaker": "Circuit breaker", "regime_filter": "Market regime filter", "dxy_filter": "DXY macro filter", "all_limits_off": "Disable all limits", "silent_mode": "Silent mode", "pre_trade_validation": "Pre-trade validation", "backtest_analysis": "Backtest analysis", "correlation_limit": "Correlation limit" } # === CLOUD CONNECTOR CLASS === import requests import threading class CloudConnector: def __init__(self, server_url="http://192.168.10.172:8000"): self.server_url = server_url self.enabled = True def push_state(self, app_instance): print(f"[CLOUD] push_state called, enabled={self.enabled}") if not self.enabled: return try: positions = [] if hasattr(app_instance, 'position_manager'): for pos in app_instance.position_manager.positions: if pos.get('status') == 'OPEN': positions.append({ 'pair': pos.get('pair', ''), 'direction': pos.get('direction', 'LONG'), 'grade': pos.get('grade', 'C'), 'entry_price': pos.get('entry_price', 0), 'current_price': pos.get('current_price', pos.get('entry_price', 0)), 'unrealized_pnl': pos.get('unrealized_pnl', 0) }) print(f"[CLOUD] Found {len(positions)} positions to push") daily_pnl = getattr(app_instance.position_manager, 'daily_pnl', 0) if hasattr(app_instance, 'position_manager') else 0 total_pnl = getattr(app_instance.position_manager, 'total_pnl', 0) if hasattr(app_instance, 'position_manager') else 0 data = { "positions": positions, "status": "engaged" if getattr(app_instance, 'bot_engaged', False) else "disengaged", "pnl": {"daily": daily_pnl, "total": total_pnl} } def push(): try: print(f"[CLOUD] Sending to {self.server_url}") resp = requests.post(f"{self.server_url}/api/update", json=data, timeout=3) print(f"[CLOUD] Server response: {resp.status_code}") except Exception as e: print(f"[CLOUD] Push error: {e}") threading.Thread(target=push, daemon=True).start() except Exception as e: print(f"[CLOUD] Error: {e}") # ============================= # ============================================================================ # CRASH REPORTER - Automatic error reporting to Kimi # ============================================================================ import sys import traceback as tb_module from datetime import datetime class CrashReporter: """Catches unhandled exceptions and prepares crash reports""" def __init__(self, app_instance=None): self.app = app_instance self.crashes_file = "/storage/emulated/0/_Newest Clawbot main script and log reports/Barebone/crash_reports.txt" self.original_excepthook = sys.excepthook sys.excepthook = self.handle_exception print("[CRASH REPORTER] Initialized - will catch crashes") def handle_exception(self, exc_type, exc_value, exc_traceback): """Handle uncaught exceptions""" # Format the exception error_msg = ''.join(tb_module.format_exception(exc_type, exc_value, exc_traceback)) # Create crash report timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') crash_report = f""" ╔══════════════════════════════════════════════════════════════╗ ║ 💥 ROB-BOT CRASH REPORT ║ ╠══════════════════════════════════════════════════════════════╣ ║ Time: {timestamp} ║ ╚══════════════════════════════════════════════════════════════╝ 🔴 ERROR TYPE: {exc_type.__name__} 🔴 ERROR MESSAGE: {exc_value} 📋 FULL TRACEBACK: {error_msg} 📊 APP STATE AT CRASH: """ # Add app state if available if self.app: try: state_info = self._get_app_state() crash_report += state_info except: crash_report += "• Could not retrieve app state\n" crash_report += """ ═══════════════════════════════════════════════════════════════ 💡 SEND THIS REPORT TO KIMI FOR FIX! ═══════════════════════════════════════════════════════════════ """ # Save to file try: with open(self.crashes_file, 'a') as f: f.write(crash_report) f.write("\n" + "="*60 + "\n\n") except: pass # Print to console print("\n" + crash_report) # Also show popup if app is still running if self.app and hasattr(self.app, 'show_popup'): try: short_error = str(exc_value)[:100] self.app.show_popup( "💥 APP CRASHED", f"Error: {short_error}\n\nFull report saved to:\n{self.crashes_file}\n\nSend to Kimi!", auto_dismiss=False ) except: pass # Call original exception hook self.original_excepthook(exc_type, exc_value, exc_traceback) def _get_app_state(self): """Get current app state for context""" info = [] try: if hasattr(self.app, 'bot_engaged'): info.append(f"• Bot Engaged: {self.app.bot_engaged}") if hasattr(self.app, 'paper_mode'): info.append(f"• Paper Mode: {self.app.paper_mode}") if hasattr(self.app, 'position_manager'): open_count = len([p for p in self.app.position_manager.positions if p.get('status') == 'OPEN']) info.append(f"• Open Positions: {open_count}") if hasattr(self.app, 'current_setups'): info.append(f"• Current Setups: {len(self.app.current_setups)}") if hasattr(self.app, 'auto_enable_trading'): info.append(f"• Auto Trade: {self.app.auto_enable_trading}") except Exception as e: info.append(f"• Error getting state: {e}") return "\n".join(info) if info else "• No state info available\n" # Global crash reporter instance crash_reporter = None # ============================================================================ class AlphaScannerApp(App): def show_confirmation_dialog(self, title, message, on_confirm, on_cancel=None): """Show confirmation dialog for important actions""" content = BoxLayout(orientation='vertical', padding=dp(20), spacing=dp(10)) # Color code based on action type if "DUAL" in title or "ENGAGE" in title: color = "00ff00" elif "DISENGAGE" in title or "OFF" in message: color = "ff4444" else: color = "ffaa00" content.add_widget(Label( text=f"[b][color={color}]{title}[/color][/b]\n\n[color=ffffff]{message}[/color]", markup=True, halign='center' )) btn_box = BoxLayout(size_hint_y=None, height=dp(50), spacing=dp(10)) cancel_btn = Button(text="CANCEL", background_color=[0.5, 0.5, 0.5, 1]) confirm_btn = Button(text="CONFIRM", background_color=[0.2, 0.7, 0.2, 1]) # DUAL ENTRY BUTTON dual_active = getattr(self, 'dual_entry_enabled', False) dual_text = "DUAL ENTRY: ON" if dual_active else "DUAL ENTRY: OFF" dual_color = [0.13, 0.55, 0.13, 1] if dual_active else [0.55, 0.13, 0.13, 1] dual_btn = Button( text=dual_text, size_hint_y=None, height=dp(40), background_color=dual_color, color=[1, 1, 1, 1], font_size=sp(14) ) dual_btn.bind(on_press=lambda x: self.toggle_dual_entry(x, from_modes=True)) content.add_widget(dual_btn) self.dual_entry_btn = dual_btn # DUAL ENTRY BUTTON dual_active = getattr(self, 'dual_entry_enabled', False) dual_text = 'DUAL ENTRY: ON' if dual_active else 'DUAL ENTRY: OFF' dual_color = [0.13, 0.55, 0.13, 1] if dual_active else [0.55, 0.13, 0.13, 1] dual_btn = Button( text=dual_text, size_hint_y=None, height=dp(40), background_color=dual_color, color=[1, 1, 1, 1] ) dual_btn.bind(on_press=lambda x: self.toggle_dual_entry(x, from_modes=True)) content.add_widget(dual_btn) self.dual_entry_btn = dual_btn popup = Popup(title='', content=content, size_hint=(0.8, 0.4), background_color=[0.1, 0.1, 0.12, 1]) def _cancel(instance): popup.dismiss() if on_cancel: on_cancel() def _confirm(instance): popup.dismiss() on_confirm() cancel_btn.bind(on_press=_cancel) confirm_btn.bind(on_press=_confirm) btn_box.add_widget(cancel_btn) btn_box.add_widget(confirm_btn) content.add_widget(btn_box) popup.open() def toggle_dual_entry(self, instance=None, from_modes=False): """Toggle dual entry on/off with confirmation""" if not hasattr(self, 'dual_entry_enabled'): self.dual_entry_enabled = True # DEFAULT: ON new_state = not self.dual_entry_enabled status = "ON" if new_state else "OFF" def _do_toggle(): self.dual_entry_enabled = new_state self.update_dual_entry_button() self.log_activity(f"🔥 [DUAL ENTRY] {status}", "INFO") if hasattr(self, 'show_popup'): self.show_popup(f"Dual Entry {status}", f"Dual Entry is now {status}") # Double confirmation when turning OFF from modes menu if from_modes and not new_state: def _confirm_off(): self.show_confirmation_dialog( "⚠️ CONFIRM DISABLE", "Are you SURE you want to disable Dual Entry?\n\nYou will enter 100% position immediately instead of 50% + 50% on favorable move.", _do_toggle ) self.show_confirmation_dialog( "DUAL ENTRY", f"Turn DUAL ENTRY {status}?\n\n50% first entry, 50% on 0.5% favorable move", _confirm_off ) else: self.show_confirmation_dialog( "DUAL ENTRY", f"Turn DUAL ENTRY {status}?\n\n50% first entry, 50% on 0.5% favorable move", _do_toggle ) def update_dual_entry_button(self): """Update dual entry button appearance""" if hasattr(self, 'dual_entry_btn'): is_on = getattr(self, 'dual_entry_enabled', False) if is_on: self.dual_entry_btn.text = "[b]🔥 DUAL ENTRY: ON[/b]" self.dual_entry_btn.background_color = [0.13, 0.55, 0.13, 1] else: self.dual_entry_btn.text = "[b]DUAL ENTRY: OFF[/b]" self.dual_entry_btn.background_color = [0.3, 0.3, 0.3, 1] self.dual_entry_btn.background_color = [0.7, 0.13, 0.13, 1] def _migrate_dual_entry_fields(self, dt=None): """Auto-migrate legacy positions to include dual entry fields""" print("[MIGRATION] Starting dual entry migration...") if not hasattr(self, 'position_manager') or not self.position_manager: print("[MIGRATION] ERROR: No position_manager") return total = len(self.position_manager.positions) print(f"[MIGRATION] Found {total} total positions") migrated = 0 for pos in self.position_manager.positions: status = pos.get('status', 'UNKNOWN') pair = pos.get('pair', 'UNKNOWN') has_de = 'dual_entry' in pos has_part = 'dual_entry_part' in pos if status == 'OPEN': print(f"[MIGRATION] Checking {pair}: has_dual_entry={has_de}, has_part={has_part}") if not has_de or not has_part: pos['dual_entry'] = True pos['dual_entry_part'] = 'first' pos['tranche2_filled'] = False entry = pos.get('entry_price', 0) direction = pos.get('direction', 'LONG') if direction == 'LONG': pos['dual_entry_trigger'] = entry * 1.005 else: pos['dual_entry_trigger'] = entry * 0.995 print(f"[MIGRATION] FIXED {pair}: trigger={pos['dual_entry_trigger']}") migrated += 1 print(f"[MIGRATION] Complete: {migrated} positions fixed") if migrated > 0: self.log_activity(f"[MIGRATION] Added dual entry to {migrated} legacy positions", "INFO") self.update_positions_display() def _manual_fix_dual_entry(self): """Manual fix button - forces dual entry fields on all open positions""" print("[MANUAL FIX] Starting dual entry fix...") if not hasattr(self, 'position_manager') or not self.position_manager: self.show_popup("Error", "No position manager found") return total = len(self.position_manager.positions) fixed = 0 for pos in self.position_manager.positions: if pos.get('status') == 'OPEN': pair = pos.get('pair', 'UNKNOWN') # Force add/update dual entry fields pos['dual_entry'] = True pos['dual_entry_part'] = 'first' pos['tranche2_filled'] = False entry = pos.get('entry_price', 0) direction = pos.get('direction', 'LONG') if direction == 'LONG': pos['dual_entry_trigger'] = entry * 1.005 else: pos['dual_entry_trigger'] = entry * 0.995 print(f"[MANUAL FIX] Fixed {pair}: trigger={pos['dual_entry_trigger']:.4f}") fixed += 1 msg = f"Fixed {fixed} positions with dual entry data" print(f"[MANUAL FIX] {msg}") self.log_activity(f"[DUAL ENTRY] {msg}", "INFO") self.update_positions_display() self.show_popup("Dual Entry Fixed", msg) def toggle_auto_trade_mode(self, instance=None): """Toggle auto-trading on/off""" if not hasattr(self, 'auto_enable_trading'): self.auto_enable_trading = False new_state = not self.auto_enable_trading status = "ON" if new_state else "OFF" self.auto_enable_trading = new_state # Also update the checkbox in settings if it exists if hasattr(self, 'auto_trade_checkbox'): self.auto_trade_checkbox.state = 'down' if new_state else 'normal' self.auto_trade_checkbox.background_color = (0.2, 0.8, 0.2, 1) if new_state else (0.5, 0.5, 0.5, 1) self.log_activity(f"🤖 [AUTO TRADE] {status}", "INFO") if hasattr(self, 'show_popup'): self.show_popup(f"Auto Trade {status}", f"Bot will automatically enter trades: {status}") self.save_settings() def toggle_auto_send_logs(self, instance=None): """Toggle auto-send logs every 30 minutes""" if not hasattr(self, 'auto_send_logs_enabled'): self.auto_send_logs_enabled = False new_state = not self.auto_send_logs_enabled status = "ON" if new_state else "OFF" self.auto_send_logs_enabled = new_state if new_state: # Schedule auto-send every 30 minutes if hasattr(self, '_auto_send_logs_event'): self._auto_send_logs_event.cancel() self._auto_send_logs_event = Clock.schedule_interval( lambda dt: self.send_logs_to_kimi(None, auto=True), 1800 # 30 minutes ) self.log_activity("📤 [AUTO SEND] Enabled - logs every 30 min", "INFO") else: # Cancel auto-send if hasattr(self, '_auto_send_logs_event'): self._auto_send_logs_event.cancel() self._auto_send_logs_event = None self.log_activity("📤 [AUTO SEND] Disabled", "INFO") if hasattr(self, 'show_popup'): self.show_popup(f"Auto Send Logs {status}", f"Automatic log sharing: {status}") self.save_settings() def send_logs_to_kimi(self, instance=None, auto=False): """Analyze logs and prepare summary for Kimi""" try: import os import glob from datetime import datetime log_dir = "/storage/emulated/0/_Newest Clawbot main script and log reports/Barebone/logs" # Find latest log log_files = glob.glob(os.path.join(log_dir, "FULL_SESSION_*.txt")) if not log_files: if not auto: self.show_popup("No Logs", "No log files found") return log_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) latest_log = log_files[0] # Read log with open(latest_log, 'r', errors='ignore') as f: content = f.read() lines = content.split('\n') # Analyze analysis = self._analyze_log_content(lines) # Save analysis to file for sharing report_file = latest_log.replace("FULL_SESSION_", "ANALYSIS_") with open(report_file, 'w') as f: f.write(analysis) # Log to app self.log_activity("📤 [LOGS SENT] Analysis ready for Kimi", "INFO") if not auto: # Show popup with summary summary = analysis.split("═")[2] if "═" in analysis else analysis[:500] self.show_popup("📤 Logs Ready for Kimi", f"Analysis saved to:\n{report_file}\n\nPreview:\n{summary[:300]}...") # Print to console for easy copy-paste print("\n" + "="*60) print("📊 ROB-BOT LOG ANALYSIS - SEND TO KIMI") print("="*60) print(analysis) print("="*60) print(f"\nFull analysis saved to: {report_file}") print("\n📋 Copy the analysis above and send to Kimi") print("="*60 + "\n") except Exception as e: self.log_activity(f"❌ [LOG SEND ERROR] {e}", "ERROR") if not auto: self.show_popup("Error", f"Failed to send logs: {e}") def _analyze_log_content(self, lines): """Analyze log and generate comprehensive report""" from datetime import datetime # Counters trades_executed = [] trades_success = [] trades_failed = [] setups_found = [] errors = [] warnings = [] pnl_values = [] timeouts = [] # Scan lines for line in lines: if "EXECUTING:" in line and "TRADE" in line: trades_executed.append(line) elif "SUCCESS" in line and "Trade entered" in line: trades_success.append(line) elif "FAILED" in line or "VALIDATION FAIL" in line: trades_failed.append(line) elif "SETUP FOUND" in line or "MULTI-TF SETUP" in line: setups_found.append(line) elif "ERROR" in line or "Exception" in line: errors.append(line) elif "WARN" in line or "TIMEOUT" in line: warnings.append(line) elif "PnL:" in line or "CLOSED:" in line: pnl_values.append(line) elif "timeout" in line.lower(): timeouts.append(line) # Calculate metrics total_setups = len(setups_found) total_trades = len(trades_executed) success_trades = len(trades_success) failed_trades = len(trades_failed) total_errors = len(errors) total_warnings = len(warnings) # Extract unique pairs pairs_traded = set() for line in trades_executed: if "EXECUTING:" in line: parts = line.split() for i, p in enumerate(parts): if "USDC" in p or "USDT" in p: pairs_traded.add(p.replace(":", "").replace("[b]", "")) # Build report report = f""" ╔══════════════════════════════════════════════════════════════╗ ║ 🤖 ROB-BOT PERFORMANCE ANALYSIS ║ ╠══════════════════════════════════════════════════════════════╣ ║ Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ║ ╚══════════════════════════════════════════════════════════════╝ 📊 SESSION SUMMARY ═══════════════════════════════════════════════════════════════ • Total Setups Found: {total_setups} • Trades Executed: {total_trades} • Successful Entries: {success_trades} • Failed/Rejected: {failed_trades} • Success Rate: {(success_trades/max(total_trades,1)*100):.1f}% • Errors: {total_errors} • Warnings/Timeouts: {total_warnings} 📈 PAIRS TRADED ═══════════════════════════════════════════════════════════════ {chr(10).join(['• ' + p for p in sorted(pairs_traded)]) if pairs_traded else '• None'} 🔍 KEY ISSUES DETECTED ═══════════════════════════════════════════════════════════════ {chr(10).join(['• ' + e.split(']')[-1][:80] for e in errors[:5]]) if errors else '• No critical errors'} ⚠️ WARNINGS & TIMEOUTS ═══════════════════════════════════════════════════════════════ {chr(10).join(['• ' + w.split(']')[-1][:80] for w in warnings[:5]]) if warnings else '• No warnings'} 💡 RECOMMENDATIONS ═══════════════════════════════════════════════════════════════ """ # Add recommendations based on issues recommendations = [] if failed_trades > success_trades: recommendations.append("• HIGH FAILURE RATE: Check validation settings, grades may be too restrictive") if total_setups > 0 and total_trades == 0: recommendations.append("• NO TRADES TAKEN: Auto-trade may be disabled or validation blocking all") if len([w for w in warnings if "timeout" in w.lower()]) > 3: recommendations.append("• MANY TIMEOUTS: Pairs in validation timeout - clear timeouts in TEST tab") if total_errors > 5: recommendations.append("• HIGH ERROR COUNT: Check API connection and data feeds") if not recommendations: recommendations.append("• Bot is operating normally - continue monitoring") report += chr(10).join(recommendations) # Add raw data for detailed analysis report += f""" 📋 RAW DATA FOR KIMI ═══════════════════════════════════════════════════════════════ Last 10 Trades: {chr(10).join(trades_executed[-10:]) if trades_executed else 'No trades'} Last 5 Errors: {chr(10).join(errors[-5:]) if errors else 'No errors'} ═══════════════════════════════════════════════════════════════ Send this analysis to Kimi for optimization recommendations! """ return report def toggle_weekend_lock(self, instance=None): """Toggle weekend lock on/off for testing""" if not hasattr(self, 'use_weekend_lock'): self.use_weekend_lock = True self.use_weekend_lock = not self.use_weekend_lock status = "ON" if self.use_weekend_lock else "OFF" self.log_activity(f"[WEEKEND LOCK] {status}", "INFO") if hasattr(self, 'show_popup'): if self.use_weekend_lock: self.show_popup("Weekend Lock ON", "Trading blocked Fri 17:00 - Mon 08:00") else: self.show_popup("⚠️ TEST MODE", "Weekend lock OFF - can trade anytime!") if hasattr(self, 'schedule_manager'): self.schedule_manager.use_weekend_lock = self.use_weekend_lock self.save_settings() def reset_settings_defaults(self, instance=None): """Reset to Protocol v5.0 defaults""" try: self.max_risk_pct = 3.0 self.max_leverage = 5 self.auto_trade_grade = "A+" self.use_weekend_lock = True self.use_dual_entry = True self.show_popup("✅ Defaults Restored", "Risk: 3% | Leverage: 5x | Grade: A+ | Dual: ON") except Exception as e: self.show_popup("❌ Error", str(e)) def reset_modes_defaults(self, instance=None): """Reset to safe defaults""" try: self.paper_mode = True self.live_mode = False self.auto_refresh = False self.use_weekend_lock = True self.show_popup("✅ Safe Defaults", "Mode: PAPER | Weekend Lock: ON") except Exception as e: self.show_popup("❌ Error", str(e)) def build(self): Window.clearcolor = DARK_BG self.title = "ROB-BOT v52.12.6 - BOT-FATHER EDITION" # Initialize crash reporter global crash_reporter crash_reporter = CrashReporter(self) print("[INIT] Crash reporter active - crashes will be logged") self.cloud = CloudConnector("http://192.168.10.172:8000") # === COMPREHENSIVE AUTO LOGGER === import os from datetime import datetime self.auto_log_dir = "/storage/emulated/0/_Newest Clawbot main script and log reports/Barebone/logs" os.makedirs(self.auto_log_dir, exist_ok=True) self.auto_log_file = os.path.join(self.auto_log_dir, f"FULL_SESSION_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt") with open(self.auto_log_file, 'w') as f: f.write(f"{'='*80}\n") f.write(f"ROB-BOT FULL SESSION LOG\n") f.write(f"Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"{'='*80}\n\n") # ============================================================================ # OPTIMAL DEFAULT SETTINGS - v52.12.6 # ============================================================================ # These settings are optimized for: # - Profitability: Target 2:1 R/R, focus on high-quality setups # - Protection: Breakeven at 0.8%, profit lock at $6, time exit at 6h # - Efficiency: Time-aware scanning (3/5/10 min intervals) # - Safety: Paper mode default, strict validation, circuit breakers # ============================================================================ # --- CORE MODE SETTINGS --- self.bot_engaged = True # Auto-engage on startup self.robot_scanner_mode = True # Robot Scanner mode enabled self.paper_mode = True # DEFAULT: Paper mode (safe testing) self.test_mode = False # Test mode off (use paper instead) # --- CAPITAL SETTINGS --- self.total_asset = 10000.0 # Total trading capital self.paper_total_asset = 10000.0 # Paper mode capital self.paper_usdc_allocation = 5000.0 # USDC allocation self.paper_usdt_allocation = 5000.0 # USDT allocation (unused, USDC only) # --- RISK & POSITION SETTINGS --- self.max_simultaneous = 3 # Max 3 concurrent positions self.max_trades_per_cycle = 3 # Take multiple trades per scan self.risk_per_trade = 0.015 # 1.5% risk per trade self.max_position_pct = 0.20 # Max 20% of capital per position self.leverage = 3 # Default 3X leverage self.max_leverage = 5 # Max selectable leverage self.order_type = "Auto" # Auto market/limit selection self.trade_direction = "BOTH" # Trade both long and short self.smart_entry_enabled = True # Smart entry timing self.auto_enable_trading = True # AUTO TRADE ON BY DEFAULT self.auto_send_logs_enabled = False # Auto-send logs to Kimi (default OFF) # --- PROFIT PROTECTION --- self.profit_protection_enabled = True # Enable all protection features self.breakeven_trigger_pct = 0.8 # Move SL to breakeven at 0.8% profit self.breakeven_profit_pct = 0.2 # +0.2% profit at breakeven level self.dollar_profit_protection = True # Enable $-based profit lock self.profit_lock_trigger = 6.0 # Lock profit when $6 unrealized self.profit_lock_amount = 3.0 # Lock $3 minimum profit self.time_exit_hours = 6 # Time exit after 6 hours self.time_exit_min_profit = 0.3 # Only exit if >0.3% profit # --- TP/SL CONFIGURATION --- self.tp1_pct = 0.75 # 75% of position at TP1 self.tp2_pct = 0.25 # 25% continues with trailing self.move_sl_to_tp1 = True # Move SL to TP1 after TP1 hit self.trail_pct = 0.005 # 0.5% trailing for TP2 self.sl_buffer_after_tp1 = 0.001 # 0.1% buffer when moving SL self.target_rr_ratio = 2.0 # Minimum 2:1 R/R for entries self.min_rr_ratio = 2.0 # Same as above (validation) # --- SCANNING SETTINGS --- self.auto_refresh = True # Auto-refresh enabled self.continuous_scan = True # Continuous scanning self.auto_scan_interval = 60 # Legacy: 60s (SetupScout overrides) self.no_setups_retry_delay = 45 # 45s retry if no setups found # --- FILTER & VALIDATION SETTINGS --- self.filter_mode = "OFF" # Bot handles filtering internally self.min_grade_filter = "F" # Allow ALL grades (F and above) self.min_trade_grade = "F" # Allow ALL grades for trading self.market_brain_enabled = True # Market Brain ON self.market_brain_strict = True # Strict mode (quality over quantity) # --- LOGGING & MONITORING SETTINGS --- self.detailed_logging_enabled = True # Enable all detailed logs self.log_all_validations = True # Log every validation check self.log_all_indicators = True # Log all indicator calculations self.log_all_entries = True # Log all entry attempts self.log_all_exits = True # Log all exits with reason self.log_performance_metrics = True # Log scan/validation performance self.save_all_setups = True # Save all setups for analysis self.save_rejected_setups = True # Save rejected setups with reasons self.activity_log_verbose = True # Verbose activity logging self.debug_mode = True # Enable debug output # Initialize behavior logger for LOGS tab self.behavior_logger = get_behavior_logger(self) # --- WEEKEND MODE --- self.weekend_mode_enabled = True # Reduce risk on weekends self.weekend_size_reduction = 0.5 # 50% position size on weekends self.weekend_leverage_reduction = 0.67 # 2X instead of 3X # --- DUAL ENTRY SETTINGS --- self.dual_entry_enabled = True # DUAL ENTRY: 50% now, 50% on 0.5% move self.dual_entry_pending = {} # Track pending second entries # --- DISABLED FEATURES --- self.compound_trading = False # Compound trading OFF self.scalping_mode_enabled = False # Scalping mode OFF self.money_maker_mode = False # Money Maker mode OFF # --- STATE VARIABLES (Do Not Modify) --- self.last_auto_scan = 0 self.initial_scan_done = False self.is_scanning = False self.is_trading = False self._filter_lock = threading.Lock() self.validation_timeouts = {} self.validation_timeout_minutes = 15 # --- INDICATOR & ANALYSIS SETTINGS --- self.lower_tf_priority = True # Weight lower timeframes more (reversal catching) self.min_consensus = 4 # 4 of 6 timeframes must agree self.pre_trade_validation = False # OFF for faster entry (BOT-FATHER handles it) self.circuit_breaker_enabled = True self.circuit_breaker_max_daily_loss = 0.05 # 5% max daily loss self.trailing_stop_pct = 0.1 # 10% trailing stop self.silent_mode = False self.all_limits_off = False # --- ENTRY TIMING SETTINGS --- self.entry_timing_enabled = True # Enable RSI/StochRSI + Orderbook validation self.entry_timing_strict = True # Reject if score < 60 (False = warn only) self.entry_rsi_period = 14 # RSI calculation period self.entry_stochrsi_period = 14 # StochRSI lookback self.entry_ob_depth = 10 # Orderbook levels to analyze self.entry_min_score = 60 # Minimum entry timing score (0-100) self.entry_wait_for_pullback = True # Wait for RSI/StochRSI pullback before entry self.entry_max_rsi_long = 70 # Max RSI for LONG entries (allow momentum) self.entry_min_rsi_short = 30 # Min RSI for SHORT entries (allow momentum) # --- ADAPTIVE ENTRY SETTINGS --- self.adaptive_entry_enabled = True # Enable adaptive entry engine self.adaptive_min_score = 55 # Minimum adaptive score (0-100) self.adaptive_scalp_enabled = True # Allow auto-scalp mode self.adaptive_momentum_min_score = 70 # Higher threshold for momentum mode self.adaptive_check_btc = True # Check BTC for macro alignment self.adaptive_check_dxy = True # Check DXY for risk sentiment self.adaptive_require_confirmation = True # Require BTC/DXY confirmation self.adaptive_sl_multiplier_scalp = 0.5 # 0.5% stop for scalp self.adaptive_tp_multiplier_scalp = 0.8 # 0.8% target for scalp self.adaptive_sl_multiplier_momentum = 1.5 # 1.5% stop for momentum self.adaptive_tp_multiplier_momentum = 3.0 # 3% target for momentum self.adaptive_sl_multiplier_meanrev = 2.0 # 2% stop for mean reversion self.adaptive_tp_multiplier_meanrev = 2.0 # 2% target for mean reversion # --- DISABLED FEATURES --- self.dxy_filter_enabled = False self.regime_filter_enabled = False self.thor_mode_enabled = False self.bulltrap_enabled = False self.rotation_enabled = False self.liquidity_enabled = False self.whale_enabled = False self.spoofwall_enabled = False # --- API SETTINGS --- self.setup_search_paused = False self._pre_pause_auto_refresh = True self.api_key = "" self.api_secret = "" self.api_key_encrypted = False self.user_password_hash = None # For app password protection self.user_email = "" # For password reset self.user_phone = "" # For SMS reset self.google_auth_enabled = False # Google Authenticator self.live_trading_configured = False # Track if live trading is set up # Asset configuration for live trading self.usdc_balance = 0.0 self.usdt_balance = 0.0 self.usdc_enabled = True self.usdt_enabled = True self.usdc_trade_size = 0.0 # Calculated from max_position_pct self.usdt_trade_size = 0.0 self.current_tab = "ROB-BOT" self.binance = BinanceClient() # DXY removed in minimal version self.dxy_filter_enabled = False # Regime detector removed in minimal version self.regime_filter_enabled = False self.regime_use_advanced = False # THOR/IndicaTHOR removed in minimal version self.thor_indicator = None self.indicathor = None self.indicathor_enabled = False self.thor_mode_enabled = False self._sentinel_cache = {} self._sentinel_cache_time = 0 self._sentinel_cache_ttl = 30 self._sentinel_fetching = False self.position_manager = PositionManager( risk_per_trade=self.risk_per_trade, dxy_indicator=None, trade_logger=None ) # Set default leverage in position manager self.position_manager.leverage = self.leverage # ============================================ # AUTO-MIGRATE: Add dual entry fields to legacy positions # ============================================ Clock.schedule_once(self._migrate_dual_entry_fields, 0.5) # Run 0.5 seconds after startup # ============================================ # SETUP QUEUE SYSTEM - Producer/Consumer Architecture # ============================================ self.setup_queue = SetupQueue(max_age_seconds=300, max_queue_size=100) self.setup_queue.register_callback(self._on_new_setup) self._queue_refresh_interval = 30 # Refresh orderbook every 30s self._last_queue_refresh = 0 self._scan_thread = None self._scan_running = False Clock.schedule_interval(self._queue_maintenance, 30) # Every 30 seconds (reduced from 10s) # ============================================ # Market analyzer and volume rotator removed in minimal version self.sound_manager = SoundManager() # ============================================ # SELF-HEALING ARCHITECTURE v1.0 # ============================================ self.self_healing_enabled = True self.learning_mode = True # Start with relaxed filters # RELAXED INITIAL PARAMETERS (Learning Mode) self.min_score_threshold = 30 # was 35 self.min_rr_ratio_learning = 2.0 # was 2.5 self.timeframe_alignment_required = 1 # was 2 self.volume_threshold = 500000 # was 750000 # Initialize self-healing if available if SELF_HEALING_AVAILABLE: try: self.self_healing = integrate_self_healing(self) Clock.schedule_interval(self._self_healing_heartbeat, 300) # 5 min print("[SELF-HEALING] Initialized successfully") except Exception as e: print(f"[SELF-HEALING] Init error: {e}") self.self_healing = None else: self.self_healing = None if hasattr(self, 'sound_settings'): self.sound_manager.enabled = self.sound_settings.get('enabled', True) self.sound_manager.set_theme(self.sound_settings.get('theme', 'pro_trader')) self.sound_manager.vibration_enabled = self.sound_settings.get('vibration', True) self.market_data = {} self.current_setups = [] self.disqualified_pairs = [] self.monitored_setups = [] self.quote_currency = "USDC" # Default to USDC only self.enabled_pairs = TOP_PAIRS.copy() + TOP_PAIRS_USDT.copy() self.blocked_pairs = [] self.pair_checkboxes = {} self.correlation_threshold = 0.85 self.time_restrictions_enabled = False self.trading_hours_mode = "24/7" self.trading_weekdays = [0, 1, 2, 3, 4] self.trading_weekend_enabled = True self.trading_start_time = (0, 0) self.trading_stop_time = (23, 59) # THOR removed in minimal version self.thor_indicator = None self.bulltrap_detector = None # SCALPING MODE - Disabled for testing self.scalping_aggression = 0.7 self.scalping_timeframes = ['1m', '3m', '5m'] self.vwap_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() # Initialize OPTIMUS BRAIN self._init_optimus_brain() # Circuit breaker stub self.circuit_breaker = CircuitBreaker() # Trade logger for analytics self.trade_logger = TradeLogger(DATA_DIR) # 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._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, 10) # Reduced from 5s to 10s Clock.schedule_interval(self.update_market_data, 30) Clock.schedule_interval(self.auto_refresh_setups, 30) # Reduced from 10s to 30s 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 _log_perf_stats(self, dt=None): try: print(f"[PERF] API calls: {self._perf_stats.get('api_calls', 0)}, Threads: {self._perf_stats.get('threads_spawned', 0)}, UI updates: {self._perf_stats.get('ui_updates', 0)}") 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.""" pair = setup.get('pair', 'Unknown') action = "UPDATED" if is_update else "NEW" print(f"[SETUP QUEUE CALLBACK] {action} setup: {pair}") # Optionally auto-refresh orderbook for high-quality setups if setup.get('grade', 'F') in ['A+', 'A', 'A-', 'B+']: Clock.schedule_once(lambda dt: self._refresh_setup_orderbook(pair), 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._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 } # 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=GRAY, 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) 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) 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) # Row 3: MODES dropdown button modes_btn = StyledButton( text="[b]MODES [v][/b]", markup=True, bg_color=GOLD, text_color=BLACK, font_size=sp(11), radius=8, size_hint_y=None, height=dp(36) ) modes_btn.bind(on_press=self.show_modes_menu) control_card.add_widget(modes_btn) 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.5)) 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.2) 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.2) 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.2) clear_btn.bind(on_press=self.clear_bot_log) log_header.add_widget(clear_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', 500000), "500K") # Get Money Maker status mm_active = getattr(self, 'money_maker_mode', False) mm_status = "💰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 = "🗡️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"🚜 Money Bulldozer: {mm_status}", 'money_maker', False), (f"🗡️ Falling Knife: {fk_status}", 'falling_knife', False), (f"🔀 DUAL ENTRY: {dual_status}", 'dual_entry', dual_active), (f"🤖 AUTO TRADE: {auto_trade_status}", 'auto_trade', auto_trade_active), (f"📤 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), ("🔧 FIX DUAL ENTRY", 'fix_dual_entry', 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))) 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', 500000) for val, label in thresholds: is_current = val == current_threshold btn = StyledButton( text=f"[b]{'[✓] ' 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', 500000) old_label = {0: "None", 250000: "250K", 500000: "500K", 750000: "750K", 1000000: "1M"}.get(old_val, "500K") 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]⚠️ 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 = "🚨" if trap_warning['score'] >= 80 else "⚠️" 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]⚠️ 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) self.active_pos_header = EmojiLabel(text="[b]ACTIVE POSITIONS (0)[/b]", markup=True, color=GOLD, font_size=sp(15), size_hint_y=None, height=dp(28)) header_card.add_widget(self.active_pos_header) 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) header_card.add_widget(close_all_btn) self.positions_screen.add_widget(header_card) # ============================================ # DAILY P&L SUMMARY CARD (no headline) # ============================================ self.pnl_summary_card = BorderedCard(size_hint_y=None, height=dp(70), padding=dp(8)) self.pnl_summary_card.orientation = 'vertical' # Row 1: Realized and Unrealized row1 = BoxLayout(orientation='horizontal', size_hint_y=0.5) self.daily_realized_lbl = Label( text="[b]Realized: $0[/b]", markup=True, color=WHITE, font_size=sp(11), size_hint_x=0.5, halign='center', valign='middle' ) row1.add_widget(self.daily_realized_lbl) self.daily_unrealized_lbl = Label( text="[b]Unrealized: $0[/b]", markup=True, color=WHITE, font_size=sp(11), size_hint_x=0.5, halign='center', valign='middle' ) row1.add_widget(self.daily_unrealized_lbl) self.pnl_summary_card.add_widget(row1) # Row 2: Today's Net P&L with Reset button row2 = BoxLayout(orientation='horizontal', size_hint_y=0.5) self.daily_net_lbl = Label( text="[b]Today's Net: $0[/b]", markup=True, color=WHITE, font_size=sp(13), size_hint_x=0.7, halign='center', valign='middle' ) row2.add_widget(self.daily_net_lbl) # Reset PnL button reset_pnl_btn = StyledButton( text="[b]RESET[/b]", markup=True, bg_color=(0.3, 0.3, 0.3, 1), text_color=WHITE, font_size=sp(9), radius=6, size_hint_x=0.3 ) reset_pnl_btn.bind(on_press=self._reset_pnl_manual) row2.add_widget(reset_pnl_btn) self.pnl_summary_card.add_widget(row2) self.positions_screen.add_widget(self.pnl_summary_card) pos_scroll = ScrollView(size_hint_y=0.68) self.positions_container = BoxLayout(orientation='vertical', size_hint_y=None, spacing=dp(8)) self.positions_container.bind(minimum_height=self.positions_container.setter('height')) pos_scroll.add_widget(self.positions_container) self.positions_screen.add_widget(pos_scroll) def _update_pos_header(self, instance, value): self.pos_header_rect.pos = instance.pos self.pos_header_rect.size = instance.size self.pos_header_border.rounded_rectangle = (instance.x, instance.y, instance.width, instance.height, dp(8)) def build_assets_screen(self): self.assets_screen = BoxLayout(orientation='vertical', padding=dp(8), spacing=dp(6)) # ============================================ # P&L CARD (moved from positions tab, reduced spacing) # ============================================ pnl_card = BorderedCard(size_hint_y=None, height=dp(200)) pnl_card.orientation = 'vertical' pnl_card.add_widget(EmojiLabel(text="[b]PROFIT & LOSS[/b]", markup=True, color=GOLD, font_size=sp(12), size_hint_y=None, height=dp(24))) # Reduced spacing from (dp(20), dp(12)) to (dp(10), dp(4)) pnl_grid = GridLayout(cols=2, spacing=(dp(10), dp(4)), padding=dp(8), size_hint_y=None, height=dp(110)) self.daily_pnl_lbl = Label(text="Daily: $0", color=WHITE, font_size=sp(12), halign='left', valign='middle') self.weekly_pnl_lbl = Label(text="Weekly: $0", color=WHITE, font_size=sp(12), halign='right', valign='middle') self.total_pnl_lbl = Label(text="Total: $0", color=WHITE, font_size=sp(12), halign='left', valign='middle') self.unrealized_pnl_lbl = Label(text="Unrealized: $0", color=GRAY, font_size=sp(12), halign='right', valign='middle') self.notional_value_lbl = Label(text="Notional: $0", markup=True, color=CYAN, font_size=sp(12), halign='left', valign='middle') self.leverage_display_lbl = Label(text="Leverage: 3X", markup=True, color=CYAN, font_size=sp(12), halign='right', valign='middle') pnl_grid.add_widget(self.daily_pnl_lbl) pnl_grid.add_widget(self.weekly_pnl_lbl) pnl_grid.add_widget(self.total_pnl_lbl) pnl_grid.add_widget(self.unrealized_pnl_lbl) pnl_grid.add_widget(self.notional_value_lbl) pnl_grid.add_widget(self.leverage_display_lbl) pnl_card.add_widget(pnl_grid) # Last reset info label self.pnl_reset_lbl = Label( text="[color=888888]Last Reset: Today[/color]", markup=True, font_size=sp(9), color=GRAY, size_hint_y=None, height=dp(16) ) pnl_card.add_widget(self.pnl_reset_lbl) # Buttons row btn_row = BoxLayout(size_hint_y=None, height=dp(36), spacing=dp(8), padding=(dp(8), dp(2))) reset_pnl_btn = StyledButton( text="[b]RESET P&L[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(10), radius=8, size_hint_x=0.45 ) reset_pnl_btn.bind(on_press=self.reset_pnl_callback) btn_row.add_widget(reset_pnl_btn) report_btn = StyledButton( text="[b]CREATE REPORT[/b]", markup=True, bg_color=BLUE, text_color=WHITE, font_size=sp(10), radius=8, size_hint_x=0.55 ) report_btn.bind(on_press=self.create_report) btn_row.add_widget(report_btn) pnl_card.add_widget(btn_row) self.assets_screen.add_widget(pnl_card) # Header card header = BorderedCard(size_hint_y=None, height=dp(60)) header.orientation = 'vertical' header.add_widget(EmojiLabel(text="[b]ASSET TRACKER[/b]", markup=True, color=GOLD, font_size=sp(14), size_hint_y=None, height=dp(24))) self.asset_summary_lbl = Label(text="Exp:$0 A:0% Av:$0 O:0/2", markup=True, color=WHITE, font_size=sp(10), size_hint_y=None, height=dp(20)) header.add_widget(self.asset_summary_lbl) self.assets_screen.add_widget(header) assets_scroll = ScrollView(size_hint_y=0.90) self.assets_container = BoxLayout(orientation='vertical', size_hint_y=None, spacing=dp(8)) self.assets_container.bind(minimum_height=self.assets_container.setter('height')) assets_scroll.add_widget(self.assets_container) self.assets_screen.add_widget(assets_scroll) @mainthread def update_assets_display(self, dt=None): self.assets_container.clear_widgets() open_positions = [p for p in self.position_manager.positions if p['status'] == 'OPEN'] total_exposure = sum(p['position_value'] for p in open_positions) allocation_pct = (total_exposure / self.total_asset * 100) if self.total_asset > 0 else 0 available = self.position_manager.get_available_balance(self.total_asset) # Shorter format to prevent overflow self.asset_summary_lbl.text = f"[b]Exp:${total_exposure:,.2f} A:{allocation_pct:.0f}% Av:${available:,.2f} O:{len(open_positions)}/{self.max_simultaneous}[/b]" # Update P&L card labels (now in assets screen instead of positions) total_unrealized = sum(p.get('unrealized_pnl', 0) for p in open_positions) realized_pnl = getattr(self, 'daily_pnl', 0) * self.total_asset weekly_pnl = getattr(self, 'weekly_pnl', 0) * self.total_asset total_pnl = getattr(self, 'total_pnl', 0) * self.total_asset # Calculate notional value notional_value = sum(p.get('position_value', 0) * p.get('leverage_used', 1) for p in open_positions) avg_lev = 0 if open_positions: leverages = [p.get('leverage_used', 1) for p in open_positions] avg_lev = sum(leverages) / len(leverages) # Update P&L labels if they exist - European format def format_european(value): return f"{abs(value):,.0f}".replace(",", "X").replace(".", ",").replace("X", ".") if hasattr(self, 'daily_pnl_lbl'): sign = '+' if realized_pnl >= 0 else '-' self.daily_pnl_lbl.text = f"Daily: {sign}${format_european(realized_pnl)}" self.daily_pnl_lbl.color = GREEN if realized_pnl >= 0 else RED if hasattr(self, 'weekly_pnl_lbl'): sign = '+' if weekly_pnl >= 0 else '-' self.weekly_pnl_lbl.text = f"Weekly: {sign}${format_european(weekly_pnl)}" self.weekly_pnl_lbl.color = GREEN if weekly_pnl >= 0 else RED if hasattr(self, 'total_pnl_lbl'): sign = '+' if total_pnl >= 0 else '-' self.total_pnl_lbl.text = f"Total: {sign}${format_european(total_pnl)}" self.total_pnl_lbl.color = GREEN if total_pnl >= 0 else RED if hasattr(self, 'unrealized_pnl_lbl'): sign = '+' if total_unrealized >= 0 else '-' self.unrealized_pnl_lbl.text = f"Unrealized: {sign}${format_european(total_unrealized)}" self.unrealized_pnl_lbl.color = GRAY if total_unrealized == 0 else (GREEN if total_unrealized >= 0 else RED) if hasattr(self, 'notional_value_lbl'): self.notional_value_lbl.text = f"Notional: ${format_european(notional_value)}" if hasattr(self, 'leverage_display_lbl'): self.leverage_display_lbl.text = f"Leverage: {avg_lev:.1f}X" core_alloc = self.total_asset * 0.30 core_used = total_exposure # Get actual leverage from positions if available avg_leverage = 3 if open_positions: leverages = [p.get('leverage_used', 3) for p in open_positions] avg_leverage = sum(leverages) / len(leverages) core_card = BorderedCard(size_hint_y=None, height=dp(100)) core_card.orientation = 'vertical' core_card.add_widget(EmojiLabel(text="[b]POSITION ALLOCATION[/b]", markup=True, color=GOLD, font_size=sp(11))) core_card.add_widget(Label(text=f"${core_used:,.0f} / ${core_alloc:,.0f} (30% max)", color=WHITE, font_size=sp(10))) core_card.add_widget(Label(text=f"Avg Lev: {avg_leverage:.1f}x | Max {self.max_simultaneous} positions", color=GRAY, font_size=sp(9))) # Show Market Brain bias if available if hasattr(self, 'market_brain') and self.market_brain: # Handle both string and enum types market_type = self.market_brain.market_type risk_mode = self.market_brain.risk_mode mt_val = market_type.value if hasattr(market_type, 'value') else str(market_type) rm_val = risk_mode.value if hasattr(risk_mode, 'value') else str(risk_mode) brain_state = f"Brain: {mt_val} | {rm_val}" core_card.add_widget(Label(text=brain_state, color=CYAN, font_size=sp(9))) self.assets_container.add_widget(core_card) risk_amount = self.total_asset * self.risk_per_trade risk_card = BorderedCard(size_hint_y=None, height=dp(80)) risk_card.orientation = 'vertical' risk_card.add_widget(Label(text="[b]RISK PER TRADE[/b]", markup=True, color=RED, font_size=sp(11))) risk_card.add_widget(Label(text=f"${risk_amount:,.2f} ({self.risk_per_trade*100:.0f}%)", color=WHITE, font_size=sp(10))) self.assets_container.add_widget(risk_card) avail_card = BorderedCard(size_hint_y=None, height=dp(80)) avail_card.orientation = 'vertical' avail_card.add_widget(Label(text="[b]AVAILABLE[/b]", markup=True, color=GREEN, font_size=sp(11))) avail_card.add_widget(Label(text=f"${available:,.0f}", color=WHITE, font_size=sp(10))) self.assets_container.add_widget(avail_card) strategy_card = BorderedCard(size_hint_y=None, height=dp(120)) strategy_card.orientation = 'vertical' strategy_card.add_widget(Label(text="[b]TRADE STRATEGY[/b]", markup=True, color=CYAN, font_size=sp(11))) strategy_card.add_widget(Label(text="TP1: 50% at target", color=WHITE, font_size=sp(9))) strategy_card.add_widget(Label(text="TP2: 50% with 0.3% trailing stop (starts above TP1)", color=WHITE, font_size=sp(9))) strategy_card.add_widget(Label(text="SL: Initial stop (cancelled after TP1, trail protects)", color=WHITE, font_size=sp(9))) self.assets_container.add_widget(strategy_card) export_card = BorderedCard(size_hint_y=None, height=dp(70)) export_card.orientation = 'vertical' export_btn = StyledButton( text="[b]EXPORT ASSET LOG[/b]", markup=True, bg_color=BLUE, text_color=WHITE, font_size=sp(11), radius=10, size_hint_x=0.7, pos_hint={'center_x': 0.5} ) export_btn.bind(on_press=self.export_asset_log) export_card.add_widget(export_btn) self.assets_container.add_widget(export_card) if open_positions: pos_header = Label(text="[b]ACTIVE POSITIONS[/b]", markup=True, color=GOLD, font_size=sp(12), size_hint_y=None, height=dp(30)) self.assets_container.add_widget(pos_header) for pos in open_positions: pos_card = BorderedCard(size_hint_y=None, height=dp(85)) pos_card.orientation = 'horizontal' remaining = pos['quantity'] - pos.get('closed_quantity', 0) # Get actual leverage and pattern from position actual_lev = pos.get('leverage_used', 5) pattern = pos.get('trade_pattern', 'standard') size_mult = pos.get('size_multiplier', 1.0) # Left side: Pair info with pattern left_box = BoxLayout(orientation='vertical', size_hint_x=0.5) left_box.add_widget(Label( text=f"[b]{pos['pair']}[/b] {pos['direction']}", markup=True, color=WHITE, font_size=sp(10) )) # === DUAL ENTRY STATUS === dual_status = "" # Check if position has dual entry data, OR if dual entry is enabled and position is new has_dual_data = pos.get('dual_entry') or pos.get('dual_entry_part') dual_entry_enabled = getattr(self, 'dual_entry_enabled', True) if has_dual_data: tranche2_filled = pos.get('tranche2_filled', False) dual_part = pos.get('dual_entry_part', '') if tranche2_filled: dual_status = "[color=00FF00]DUAL ✓[/color] " elif dual_part == 'first': dual_status = "[color=FFA500]DUAL 1/2[/color] " elif dual_part == 'second': dual_status = "[color=00FF00]DUAL 2/2[/color] " else: dual_status = "[color=FFA500]DUAL...[/color] " elif dual_entry_enabled and pos.get('status') == 'OPEN': # Dual entry is enabled but position doesn't have data yet - auto-fix on display dual_status = "[color=FFA500]DUAL 1/2[/color] " left_box.add_widget(Label( text=f"{dual_status}{actual_lev}x | {pattern} | {size_mult:.1f}x", markup=True, color=GRAY, font_size=sp(8) )) pos_card.add_widget(left_box) # Middle: Value and P&L unrealized = pos.get('unrealized_pnl', 0) mid_box = BoxLayout(orientation='vertical', size_hint_x=0.3) mid_box.add_widget(Label( text=f"${pos['position_value']:,.0f}", color=GREEN if unrealized >= 0 else RED, font_size=sp(10) )) mid_box.add_widget(Label( text=f"{'+' if unrealized >= 0 else ''}${unrealized:.2f}", color=GREEN if unrealized >= 0 else RED, font_size=sp(8) )) pos_card.add_widget(mid_box) # Right: TP status tp1_icon = "DONE" if pos.get('tp1_hit') else "OPEN" pos_card.add_widget(Label( text=f"TP1:{tp1_icon}", color=GREEN if pos.get('tp1_hit') else GRAY, font_size=sp(10), size_hint_x=0.2 )) self.assets_container.add_widget(pos_card) # === HISTORICAL TRADES === hist_header = Label(text="[b]HISTORICAL TRADES[/b]", markup=True, color=GOLD, font_size=sp(12), size_hint_y=None, height=dp(30)) self.assets_container.add_widget(hist_header) # Historical trades card with log button hist_card = BorderedCard(size_hint_y=None, height=dp(120)) hist_card.orientation = 'vertical' hist_card.padding = dp(8) hist_card.spacing = dp(4) # Trade stats summary total_trades, win_count, loss_count, win_rate = self._get_trade_stats() hist_stats = BoxLayout(size_hint_y=None, height=dp(50)) hist_stats.add_widget(Label(text=f"Total: {total_trades}", color=WHITE, font_size=sp(11), size_hint_x=0.25)) hist_stats.add_widget(Label(text=f"[color=00FF00]Wins: {win_count}[/color]", markup=True, font_size=sp(11), size_hint_x=0.25)) hist_stats.add_widget(Label(text=f"[color=FF0000]Losses: {loss_count}[/color]", markup=True, font_size=sp(11), size_hint_x=0.25)) hist_stats.add_widget(Label(text=f"Rate: {win_rate:.0f}%", color=WHITE, font_size=sp(11), size_hint_x=0.25)) hist_card.add_widget(hist_stats) # Buttons row hist_btn_row = BoxLayout(size_hint_y=None, height=dp(40), spacing=dp(8)) hist_log_btn = StyledButton( text="[b]LOG TRADES[/b]", markup=True, bg_color=BLUE, text_color=WHITE, font_size=sp(10), radius=6 ) hist_log_btn.bind(on_press=self._log_historical_trades) hist_btn_row.add_widget(hist_log_btn) hist_view_btn = StyledButton( text="[b]VIEW ALL[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(10), radius=6 ) hist_view_btn.bind(on_press=self._view_all_trades) hist_btn_row.add_widget(hist_view_btn) hist_card.add_widget(hist_btn_row) self.assets_container.add_widget(hist_card) self.assets_container.height = max(len(self.assets_container.children) * dp(90), dp(500)) def update_sentinel_monitor(self): if not hasattr(self, 'sentinel_active_count'): return active = sum([ getattr(self, 'rotation_enabled', False), getattr(self, 'liquidity_enabled', False), getattr(self, 'whale_enabled', False), getattr(self, 'spoofwall_enabled', False) ]) self.sentinel_active_count.text = f"{active}/4 Active" if active > 0: self.sentinel_status_indicator.color = GREEN self.sentinel_active_count.color = GREEN else: self.sentinel_status_indicator.color = GRAY self.sentinel_active_count.color = GRAY if getattr(self, 'rotation_enabled', False): self.sentinel_rotation_icon.color = CYAN self.sentinel_rotation_txt.color = WHITE # bias removed rotation_phase = bias.get('bias', 'NEUTRAL')[:3] self.sentinel_rotation_txt.text = rotation_phase else: self.sentinel_rotation_icon.color = GRAY self.sentinel_rotation_txt.color = GRAY self.sentinel_rotation_txt.text = "--" if getattr(self, 'liquidity_enabled', False): self.sentinel_liquidity_icon.color = GREEN self.sentinel_liquidity_txt.color = WHITE avg_liq = getattr(self, '_last_liquidity_score', 0) self.sentinel_liquidity_txt.text = f"{avg_liq:.0f}" if avg_liq > 0 else "OK" else: self.sentinel_liquidity_icon.color = GRAY self.sentinel_liquidity_txt.color = GRAY self.sentinel_liquidity_txt.text = "--" if getattr(self, 'whale_enabled', False): self.sentinel_whale_icon.color = AMBER self.sentinel_whale_txt.color = WHITE whale_count = getattr(self, '_recent_whale_signals', 0) self.sentinel_whale_txt.text = f"{whale_count}" if whale_count > 0 else "OK" else: self.sentinel_whale_icon.color = GRAY self.sentinel_whale_txt.color = GRAY self.sentinel_whale_txt.text = "--" if getattr(self, 'spoofwall_enabled', False): self.sentinel_spoofwall_icon.color = RED self.sentinel_spoofwall_txt.color = WHITE spoof_count = getattr(self, '_recent_spoof_signals', 0) self.sentinel_spoofwall_txt.text = f"{spoof_count}" if spoof_count > 0 else "CLR" else: self.sentinel_spoofwall_icon.color = GRAY self.sentinel_spoofwall_txt.color = GRAY self.sentinel_spoofwall_txt.text = "--" if active == 0: self.sentinel_alert_lbl.text = "Tap SENTINEL tab to configure modules" self.sentinel_alert_lbl.color = GRAY else: self.sentinel_alert_lbl.text = f"SENTINEL active: Monitoring {active} module(s)" self.sentinel_alert_lbl.color = GREEN @mainthread def analyze_rotation(self, setup): if not getattr(self, 'rotation_enabled', False): return True, None # bias removed setup_direction = setup.get('direction', 'LONG') if bias['bias'] == 'BULLISH' and setup_direction == 'SHORT': return False, f"Rotation: Market BULLISH, avoiding SHORT" if bias['bias'] == 'BEARISH' and setup_direction == 'LONG': return False, f"Rotation: Market BEARISH, avoiding LONG" return True, f"Rotation: Aligned with {bias['bias']} bias" def analyze_liquidity(self, setup): if not getattr(self, 'liquidity_enabled', False): return True, None pair = setup.get('pair', '') cache_key = f"liquidity_{pair}" if cache_key in self._sentinel_cache: cached = self._sentinel_cache[cache_key] self._last_liquidity_score = cached.get('score', 50) return cached['result'], cached['msg'] if not self._sentinel_fetching: threading.Thread(target=self._fetch_sentinel_data, args=(pair,), daemon=True).start() return True, "Liquidity: Checking..." def analyze_whale(self, setup): if not getattr(self, 'whale_enabled', False): return True, None pair = setup.get('pair', '') cache_key = f"whale_{pair}" if cache_key in self._sentinel_cache: cached = self._sentinel_cache[cache_key] return cached['result'], cached['msg'] if not self._sentinel_fetching: threading.Thread(target=self._fetch_sentinel_data, args=(pair,), daemon=True).start() return True, "Whale: Checking..." def analyze_spoofwall(self, setup): if not getattr(self, 'spoofwall_enabled', False): return True, None pair = setup.get('pair', '') cache_key = f"spoof_{pair}" if cache_key in self._sentinel_cache: cached = self._sentinel_cache[cache_key] return cached['result'], cached['msg'] if not self._sentinel_fetching: threading.Thread(target=self._fetch_sentinel_data, args=(pair,), daemon=True).start() return True, "Spoof: Checking..." def _fetch_sentinel_data(self, pair): if self._sentinel_fetching: return self._sentinel_fetching = True try: if time.time() - self._sentinel_cache_time < self._sentinel_cache_ttl: return try: klines_1h = self.binance.get_klines(pair, '1h', 24) if klines_1h and len(klines_1h) >= 10: volumes = [float(k[5]) for k in klines_1h] avg_vol = sum(volumes) / len(volumes) current_vol = volumes[-1] vol_ratio = current_vol / avg_vol if avg_vol > 0 else 1.0 self._last_liquidity_score = min(100, vol_ratio * 50) if vol_ratio < 0.5: self._sentinel_cache[f"liquidity_{pair}"] = { 'result': False, 'msg': f"Liquidity: Low volume ({vol_ratio:.1f}x avg)", 'score': self._last_liquidity_score } else: self._sentinel_cache[f"liquidity_{pair}"] = { 'result': True, 'msg': f"Liquidity: Good ({vol_ratio:.1f}x avg)", 'score': self._last_liquidity_score } except Exception as e: print(f"[SENTINEL] Liquidity fetch error: {e}") try: klines_5m = self.binance.get_klines(pair, '5m', 12) if klines_5m and len(klines_5m) >= 5: volumes = [float(k[5]) for k in klines_5m] avg_vol = sum(volumes[:-1]) / max(1, len(volumes)-1) current_vol = volumes[-1] if current_vol > avg_vol * 3: self._recent_whale_signals = getattr(self, '_recent_whale_signals', 0) + 1 price_change = (float(klines_5m[-1][4]) - float(klines_5m[-1][1])) / float(klines_5m[-1][1]) self._sentinel_cache[f"whale_{pair}"] = { 'result': True, 'msg': f"Whale: Activity detected ({price_change*100:+.1f}%)" } else: self._sentinel_cache[f"whale_{pair}"] = { 'result': True, 'msg': "Whale: No unusual activity" } except Exception as e: print(f"[SENTINEL] Whale fetch error: {e}") try: klines_1m = self.binance.get_klines(pair, '1m', 10) if klines_1m and len(klines_1m) >= 5: prices = [float(k[4]) for k in klines_1m] reversals = 0 for i in range(2, len(prices)): if (prices[i] > prices[i-1] and prices[i-1] < prices[i-2]) or \ (prices[i] < prices[i-1] and prices[i-1] > prices[i-2]): reversals += 1 if reversals >= 3: self._recent_spoof_signals = getattr(self, '_recent_spoof_signals', 0) + 1 self._sentinel_cache[f"spoof_{pair}"] = { 'result': False, 'msg': f"Spoof: Manipulation detected ({reversals} reversals)" } else: self._sentinel_cache[f"spoof_{pair}"] = { 'result': True, 'msg': "Spoof: Clean" } except Exception as e: print(f"[SENTINEL] Spoof fetch error: {e}") self._sentinel_cache_time = time.time() finally: self._sentinel_fetching = False def run_sentinel_analysis(self, setup): if not getattr(self, 'liquidity_enabled', False): return True, None pair = setup.get('pair', '') try: klines = self.binance.get_klines(pair, '1h', 24) if not klines or len(klines) < 10: return True, "Liquidity: Insufficient data" volumes = [float(k[5]) for k in klines] avg_vol = sum(volumes) / len(volumes) current_vol = volumes[-1] vol_ratio = current_vol / avg_vol if avg_vol > 0 else 1.0 self._last_liquidity_score = min(100, vol_ratio * 50) if vol_ratio < 0.5: return False, f"Liquidity: Low volume ({vol_ratio:.1f}x avg)" return True, f"Liquidity: Good ({vol_ratio:.1f}x avg)" except Exception as e: return True, f"Liquidity: Check error" def run_sentinel_analysis(self, setup): if not any([getattr(self, 'rotation_enabled', False), getattr(self, 'liquidity_enabled', False), getattr(self, 'whale_enabled', False), getattr(self, 'spoofwall_enabled', False)]): return True, "SENTINEL: Inactive" results = [] all_passed = True checks = [ ("ROTATION", self.analyze_rotation(setup)), ("LIQUIDITY", self.analyze_liquidity(setup)), ("WHALE", self.analyze_whale(setup)), ("SPOOFWALL", self.analyze_spoofwall(setup)), ] for name, (passed, msg) in checks: if msg: results.append(msg) if not passed: all_passed = False if hasattr(self, 'sentinel_alert_lbl'): if all_passed: self.sentinel_alert_lbl.text = "✓ SENTINEL: Setup passed all checks" self.sentinel_alert_lbl.color = GREEN else: failed = [r for r in results if "SENTINEL:" not in r and any(x in r for x in ["avoiding", "Low", "pressure", "Manipulation"])] self.sentinel_alert_lbl.text = f"⚠ SENTINEL: {failed[0] if failed else 'Check failed'}" self.sentinel_alert_lbl.color = RED return all_passed, " | ".join(results) def toggle_rotation(self, instance): self.rotation_enabled = not getattr(self, 'rotation_enabled', False) self.log_activity(f"[b]ROTATION:[/b] {'ENABLED' if self.rotation_enabled else 'DISABLED'}", "INFO") self.update_sentinel_display() self.update_sentinel_monitor() def toggle_liquidity(self, instance): self.liquidity_enabled = not getattr(self, 'liquidity_enabled', False) self.log_activity(f"[b]LIQUIDITY:[/b] {'ENABLED' if self.liquidity_enabled else 'DISABLED'}", "INFO") self.update_sentinel_display() self.update_sentinel_monitor() def toggle_whale(self, instance): self.whale_enabled = not getattr(self, 'whale_enabled', False) self.log_activity(f"[b]WHALE:[/b] {'ENABLED' if self.whale_enabled else 'DISABLED'}", "INFO") self.update_sentinel_display() self.update_sentinel_monitor() def toggle_spoofwall(self, instance): self.spoofwall_enabled = not getattr(self, 'spoofwall_enabled', False) self.log_activity(f"[b]SPOOFWALL:[/b] {'ENABLED' if self.spoofwall_enabled else 'DISABLED'}", "INFO") self.update_sentinel_display() self.update_sentinel_monitor() def refresh_setups_display(self): if hasattr(self, 'disqualified_pairs') and self.disqualified_pairs: disqualified_keys = [(d.get('pair'), d.get('direction')) for d in self.disqualified_pairs] self.current_setups = [ s for s in self.current_setups if (s.get('pair'), s.get('direction')) not in disqualified_keys ] # DEBUG: Log all setups being displayed with their grades print(f"[SETUPS DISPLAY] Refreshing {len(self.current_setups)} setups. min_grade_filter={getattr(self, 'min_grade_filter', 'NOT SET')}") for i, setup in enumerate(self.current_setups[:10]): # Log first 10 print(f"[SETUPS DISPLAY] Setup {i+1}: {setup.get('pair')} {setup.get('direction')} - Grade: {setup.get('grade')} (Score: {setup.get('score')})") self.setups_container.clear_widgets() if self.current_setups: for setup in self.current_setups: card = SetupCard(setup, on_trade_callback=self.manual_trade) self.setups_container.add_widget(card) # Log trade selection process when multiple setups available if len(self.current_setups) > 1: self.log_trade_selection_process(self.current_setups, context="REFRESH_DISPLAY") best = self.current_setups[0] self.update_best_setup_display(best) self.found_setups_lbl.text = f"[b]FOUND {len(self.current_setups)} SETUP{'S' if len(self.current_setups) > 1 else ''}[/b]" else: self.found_setups_lbl.text = "" # Safely update best setup content if it exists 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 if hasattr(self, 'monitored_setups_lbl'): count = len(getattr(self, 'monitored_setups', [])) self.monitored_setups_lbl.text = f"Monitored: {count} setups | Click MONITOR SELECTED to add" if hasattr(self, 'disqualified_lbl'): count = len(getattr(self, 'disqualified_pairs', [])) self.disqualified_lbl.text = f"Disqualified: {count} pairs | Click 🚫 DQ on setup cards" def open_monitor_setup_popup(self, instance=None): content = BoxLayout(orientation='vertical', spacing=dp(10), padding=dp(15)) content.add_widget(Label( text="[b]🔍 MONITOR SETUPS[/b]", markup=True, color=CYAN, font_size=sp(14), size_hint_y=None, height=dp(30) )) if self.current_setups: setups_text = "\n".join([f"• {s.get('pair')} {s.get('direction')} (Grade: {s.get('grade')})" for s in self.current_setups[:5]]) else: setups_text = "No active setups. Run a scan first." content.add_widget(Label( text=f"[b]Available Setups:[/b]\n{setups_text}", markup=True, color=WHITE, font_size=sp(10), size_hint_y=None, height=dp(100) )) filters_box = BoxLayout(orientation='vertical', size_hint_y=None, height=dp(120), spacing=dp(5)) filters_box.add_widget(Label(text="[b]Additional Filters:[/b]", markup=True, color=GOLD, font_size=sp(11))) self.monitor_bulltrap_chk = BoxLayout(size_hint_y=None, height=dp(30)) self.monitor_bulltrap_chk.add_widget(Label(text="BULLTRAP Check", color=WHITE, font_size=sp(10), size_hint_x=0.7)) self.monitor_bulltrap_btn = StyledButton(text="[b]ON[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(9), radius=6, size_hint_x=0.3) self.monitor_bulltrap_btn.bind(on_press=lambda x: self.toggle_monitor_btn(x)) self.monitor_bulltrap_chk.add_widget(self.monitor_bulltrap_btn) filters_box.add_widget(self.monitor_bulltrap_chk) self.monitor_liquidity_chk = BoxLayout(size_hint_y=None, height=dp(30)) self.monitor_liquidity_chk.add_widget(Label(text="LIQUIDITY Check", color=WHITE, font_size=sp(10), size_hint_x=0.7)) self.monitor_liquidity_btn = StyledButton(text="[b]ON[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(9), radius=6, size_hint_x=0.3) self.monitor_liquidity_btn.bind(on_press=lambda x: self.toggle_monitor_btn(x)) self.monitor_liquidity_chk.add_widget(self.monitor_liquidity_btn) filters_box.add_widget(self.monitor_liquidity_chk) self.monitor_whale_chk = BoxLayout(size_hint_y=None, height=dp(30)) self.monitor_whale_chk.add_widget(Label(text="WHALE Check", color=WHITE, font_size=sp(10), size_hint_x=0.7)) self.monitor_whale_btn = StyledButton(text="[b]ON[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(9), radius=6, size_hint_x=0.3) self.monitor_whale_btn.bind(on_press=lambda x: self.toggle_monitor_btn(x)) self.monitor_whale_chk.add_widget(self.monitor_whale_btn) filters_box.add_widget(self.monitor_whale_chk) content.add_widget(filters_box) btn_box = BoxLayout(size_hint_y=None, height=dp(45), spacing=dp(10)) monitor_all_btn = StyledButton( text="[b]MONITOR ALL[/b]", markup=True, bg_color=BLUE, text_color=WHITE, font_size=sp(11), radius=8 ) monitor_all_btn.bind(on_press=lambda x: self.start_monitoring_setups(popup)) btn_box.add_widget(monitor_all_btn) clear_btn = StyledButton( text="[b]CLEAR[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(11), radius=8 ) clear_btn.bind(on_press=lambda x: self.clear_monitored_setups()) btn_box.add_widget(clear_btn) close_btn = StyledButton( text="[b]CLOSE[/b]", markup=True, bg_color=GRAY, text_color=WHITE, font_size=sp(11), radius=8 ) close_btn.bind(on_press=lambda x: popup.dismiss()) btn_box.add_widget(close_btn) content.add_widget(btn_box) popup = Popup( title='Monitor Setups', content=content, size_hint=(0.9, 0.7), background_color=CARD_BG ) popup.open() def toggle_monitor_btn(self, btn): if btn.text == "[b]ON[/b]": btn.text = "[b]OFF[/b]" btn.bg_color = GRAY else: btn.text = "[b]ON[/b]" btn.bg_color = GREEN def start_monitoring_setups(self, popup=None): if not self.current_setups: self.log_activity("[b]MONITOR:[/b] No setups to monitor. Run a scan first.", "WARN") if popup: popup.dismiss() return if popup: popup.dismiss() self.log_activity("[b]MONITOR:[/b] Starting background analysis of setups...", "INFO") thread = threading.Thread( target=self._monitor_setups_worker, args=(self.current_setups.copy(),), daemon=True ) thread.start() def _monitor_setups_worker(self, setups_to_monitor): monitored = [] bulltrap_enabled = hasattr(self, 'monitor_bulltrap_btn') and self.monitor_bulltrap_btn.text == "[b]ON[/b]" liquidity_enabled = hasattr(self, 'monitor_liquidity_btn') and self.monitor_liquidity_btn.text == "[b]ON[/b]" whale_enabled = hasattr(self, 'monitor_whale_btn') and self.monitor_whale_btn.text == "[b]ON[/b]" for setup in setups_to_monitor: monitored_setup = setup.copy() monitored_setup['monitor_time'] = datetime.now().isoformat() monitored_setup['filter_results'] = {} if bulltrap_enabled and hasattr(self, 'bulltrap_detector'): try: trap_check = self.bulltrap_detector.check_trap(setup.get('pair')) monitored_setup['filter_results']['bulltrap'] = trap_check except Exception as e: monitored_setup['filter_results']['bulltrap'] = {'is_trap': False, 'error': str(e)} monitored.append(monitored_setup) self._update_monitoring_complete(monitored) @mainthread def _update_monitoring_complete(self, monitored_setups): self.monitored_setups = monitored_setups self.log_activity(f"[b]MONITOR:[/b] Now monitoring {len(self.monitored_setups)} setup(s) with additional filters", "INFO") self.refresh_setups_display() def clear_monitored_setups(self): count = len(getattr(self, 'monitored_setups', [])) self.monitored_setups = [] self.log_activity(f"[b]MONITOR:[/b] Cleared {count} monitored setup(s)", "INFO") self.refresh_setups_display() def open_compare_pairs_popup(self, instance=None): from kivy.graphics import Color, Rectangle instance.canvas.before.clear() with instance.canvas.before: Color(*GOLD) Rectangle(pos=instance.pos, size=instance.size) def _update_sep2_rect(self, instance, value): from kivy.graphics import Color, Rectangle instance.canvas.before.clear() with instance.canvas.before: Color(*GOLD) Rectangle(pos=instance.pos, size=instance.size) def test_funding_display(self): self.log_activity("[b]FUNDING:[/b] Testing display with sample data...", "INFO") test_longs = [ {'pair': 'BTCUSDT', 'rate': 0.0250, 'mark_price': 65000, 'index_price': 64950}, {'pair': 'ETHUSDT', 'rate': 0.0180, 'mark_price': 3500, 'index_price': 3498}, {'pair': 'SOLUSDT', 'rate': 0.0150, 'mark_price': 150, 'index_price': 149.8}, {'pair': 'BNBUSDT', 'rate': 0.0120, 'mark_price': 600, 'index_price': 599}, {'pair': 'XRPUSDT', 'rate': 0.0100, 'mark_price': 0.60, 'index_price': 0.599}, ] test_shorts = [ {'pair': 'DOGEUSDT', 'rate': -0.0220, 'mark_price': 0.12, 'index_price': 0.1201}, {'pair': 'SHIBUSDT', 'rate': -0.0190, 'mark_price': 0.00002, 'index_price': 0.0000201}, {'pair': 'PEPEUSDT', 'rate': -0.0150, 'mark_price': 0.000001, 'index_price': 0.00000101}, ] self.funding_data = { 'longs': test_longs, 'shorts': test_shorts, 'last_update': datetime.now() } self.update_funding_display() self.log_activity(f"[b]FUNDING:[/b] Test data displayed - {len(test_longs)} longs, {len(test_shorts)} shorts", "SUCCESS") print(f"[FUNDING TEST] Set {len(test_longs)} longs and {len(test_shorts)} shorts") def _create_funding_row(self, rank, pair, rate_8h, rate_24h, color): row = BoxLayout(size_hint_y=None, height=dp(30), spacing=dp(4), padding=(4, 2)) row.add_widget(Label( text=rank, color=GRAY, font_size=sp(9), size_hint_x=0.08 )) row.add_widget(Label( text=pair, color=WHITE, font_size=sp(10), size_hint_x=0.28, halign='left' )) row.add_widget(Label( text=rate_8h, color=color, font_size=sp(9), size_hint_x=0.22 )) row.add_widget(Label( text=rate_24h, color=color, font_size=sp(9), size_hint_x=0.22 )) trade_btn = StyledButton( text="[b]TRADE[/b]", markup=True, bg_color=color, text_color=WHITE, font_size=sp(8), radius=4, size_hint_x=0.18 ) trade_btn.bind(on_press=lambda x, p=pair: self.open_funding_trade(p)) row.add_widget(trade_btn) return row def open_funding_trade(self, pair): if pair == "---" or not pair: return self.log_activity(f"[b]FUNDING:[/b] Opening trade for {pair}", "INFO") @mainthread def update_funding_rates(self, dt=None): self.fetch_funding_rates() def load_faq_content(self): try: possible_paths = [ os.path.join(os.path.dirname(__file__), 'FAQ_CONTENT.md'), os.path.join(os.getcwd(), 'FAQ_CONTENT.md'), '/storage/emulated/0/_Newest Clawbot main script and log reports/FAQ_CONTENT.md', 'FAQ_CONTENT.md' ] faq_path = None for path in possible_paths: if os.path.exists(path): faq_path = path break if faq_path: with open(faq_path, 'r', encoding='utf-8') as f: content = f.read() lines = content.split('\n') formatted = [] for line in lines: stripped = line.strip() if stripped.startswith('# '): formatted.append(f"[b]{stripped[2:]}[/b]") elif stripped.startswith('## '): formatted.append(f"[b]{stripped[3:]}[/b]") elif stripped.startswith('### '): formatted.append(f"\n[b]{stripped[4:]}[/b]") elif stripped.startswith('**') and stripped.endswith('**'): text = stripped[2:-2] formatted.append(f"[b]{text}[/b]") elif stripped.startswith('**'): text = stripped[2:].replace('**', '[/b]', 1) formatted.append(f"[b]{text}[/b]") elif stripped.startswith('• '): formatted.append(f" • {stripped[2:]}") elif stripped.startswith('- '): formatted.append(f" - {stripped[2:]}") elif stripped: formatted.append(stripped) return '\n'.join(formatted[:80]) else: return except Exception as e: return f"[b]FAQ[/b]\n\nQuick Help:\n• Start with Paper Mode\n• Set risk to 1-2% initially\n• Monitor Friday 16:00 exit\n\nError loading full FAQ: {str(e)[:50]}" def show_full_faq(self, instance=None): content = ScrollView() faq_text = self.load_faq_content() lbl = Label( text=faq_text, color=WHITE, font_size=sp(10), size_hint_y=None, text_size=(None, None), halign='left', valign='top' ) lbl.bind(texture_size=lambda instance, value: setattr(instance, 'height', value[1])) content.add_widget(lbl) popup = Popup( title='Full FAQ', content=content, size_hint=(0.9, 0.8), background_color=CARD_BG ) popup.open() def show_upgrade_popup(self, tier, price): content = BoxLayout(orientation='vertical', spacing=dp(10), padding=dp(20)) content.add_widget(Label( text=f"[b]Upgrade to {tier}?[/b]", markup=True, color=GOLD, font_size=sp(14) )) content.add_widget(Label( text=f"Price: {price}\n\nContact support to upgrade:", color=WHITE, font_size=sp(11) )) content.add_widget(Label( text="Telegram: @ROB_BOT_Support\nEmail: support@rob-bot.io", color=CYAN, font_size=sp(10) )) btn_box = BoxLayout(size_hint_y=0.3, spacing=dp(10)) close_btn = StyledButton( text="[b]CLOSE[/b]", markup=True, bg_color=GRAY, text_color=WHITE, font_size=sp(11), radius=8 ) close_btn.bind(on_press=lambda x: popup.dismiss()) btn_box.add_widget(close_btn) content.add_widget(btn_box) popup = Popup( title='Upgrade', content=content, size_hint=(0.8, 0.5), background_color=CARD_BG ) popup.open() def show_info_popup(self, title, message): content = BoxLayout(orientation='vertical', spacing=dp(10), padding=dp(20)) content.add_widget(Label( text=message, color=WHITE, font_size=sp(10) )) close_btn = StyledButton( text="[b]CLOSE[/b]", markup=True, bg_color=BLUE, text_color=WHITE, font_size=sp(11), radius=8, size_hint_y=0.25 ) content.add_widget(close_btn) popup = Popup( title=title, content=content, size_hint=(0.8, 0.5), background_color=CARD_BG ) close_btn.bind(on_press=popup.dismiss) popup.open() def build_test_screen(self): """Build the Test tab with speed tests and diagnostics""" root_scroll = ScrollView() layout = BoxLayout(orientation='vertical', size_hint_y=None, spacing=dp(4), padding=dp(6)) layout.bind(minimum_height=layout.setter('height')) # Compact Header - single line layout.add_widget(EmojiLabel( text="[b]TEST: DIAGNOSTICS[/b]", markup=True, color=GOLD, font_size=sp(12), size_hint_y=None, height=dp(24) )) # Overall Status Card - compressed self.perf_status_card = BorderedCard(size_hint_y=None, height=dp(36)) self.perf_status_card.orientation = 'vertical' self.perf_status_card.padding = dp(2) self.perf_status_card.spacing = dp(0) self.perf_overall_label = Label( text="[color=888888]Status: Waiting...[/color]", markup=True, color=WHITE, font_size=sp(10), size_hint_y=None, height=dp(24) ) self.perf_status_card.add_widget(self.perf_overall_label) layout.add_widget(self.perf_status_card) # === MARKET BRAIN DISPLAY === brain_card = BorderedCard(size_hint_y=None, height=dp(80)) brain_card.orientation = 'vertical' brain_card.padding = dp(4) brain_card.spacing = dp(2) brain_header = BoxLayout(size_hint_y=None, height=dp(20)) brain_header.add_widget(EmojiLabel(text="[b]BRAIN: MARKET[/b]", markup=True, color=PURPLE, font_size=sp(11), size_hint_x=0.6)) self.brain_status_label = Label(text="[b]--[/b]", markup=True, color=GRAY, font_size=sp(11), size_hint_x=0.4, halign='right') brain_header.add_widget(self.brain_status_label) brain_card.add_widget(brain_header) self.brain_details_label = Label( text="[color=888888]Strategy: -- | Bias: -- | Risk: --[/color]", markup=True, color=WHITE, font_size=sp(9), size_hint_y=None, height=dp(40) ) brain_card.add_widget(self.brain_details_label) # Brain decision monitor self.brain_decision_label = Label( text="[color=888888]Last Decision: --[/color]", markup=True, color=GRAY, font_size=sp(8), size_hint_y=None, height=dp(20) ) brain_card.add_widget(self.brain_decision_label) layout.add_widget(brain_card) # === MARKET BRAIN CONTROL PANEL === brain_control_card = BorderedCard(size_hint_y=None, height=dp(120)) brain_control_card.orientation = 'vertical' brain_control_card.padding = dp(6) brain_control_card.spacing = dp(4) brain_control_card.add_widget(Label( text="[b]BRAIN: CONTROL PANEL[/b]", markup=True, color=GOLD, font_size=sp(11), size_hint_y=None, height=dp(22) )) # Status row brain_status_row = BoxLayout(size_hint_y=None, height=dp(30), spacing=dp(8)) self.brain_enabled_btn = StyledButton( text="[b]ON[/b]" if self.market_brain_enabled else "[b]OFF[/b]", markup=True, bg_color=GREEN if self.market_brain_enabled else GRAY, text_color=WHITE, font_size=sp(10), radius=8, size_hint_x=0.5 ) self.brain_enabled_btn.bind(on_press=self.toggle_market_brain) brain_status_row.add_widget(self.brain_enabled_btn) self.brain_strict_btn = StyledButton( text="[b]AGGRESSIVE[/b]" if not self.market_brain_strict else "[b]CONSERVATIVE[/b]", markup=True, bg_color=AMBER if not self.market_brain_strict else BLUE, text_color=WHITE, font_size=sp(10), radius=8, size_hint_x=0.5 ) self.brain_strict_btn.bind(on_press=self.toggle_brain_strict) brain_status_row.add_widget(self.brain_strict_btn) brain_control_card.add_widget(brain_status_row) # Description label brain_control_card.add_widget(Label( text="[color=888888]Market Brain adjusts position size/leverage based on market conditions. All setups are traded.[/color]", markup=True, color=GRAY, font_size=sp(8), size_hint_y=None, height=dp(30), halign='center', valign='middle' )) # Brain stats self.brain_stats_label = Label( text="[color=888888]Trade: -- | Caution: -- | Size: -- | Lev: --[/color]", markup=True, color=WHITE, font_size=sp(9), size_hint_y=None, height=dp(24) ) brain_control_card.add_widget(self.brain_stats_label) # Regime stability label self.brain_regime_label = Label( text="[color=888888]Regime: Analyzing...[/color]", markup=True, color=WHITE, font_size=sp(8), size_hint_y=None, height=dp(20) ) brain_control_card.add_widget(self.brain_regime_label) layout.add_widget(brain_control_card) # === HIGH-SPEED BACKTEST PANEL === backtest_card = BorderedCard(size_hint_y=None, height=dp(140)) backtest_card.orientation = 'vertical' backtest_card.padding = dp(6) backtest_card.spacing = dp(4) backtest_card.add_widget(Label( text="[b]SPEED TEST: HISTORICAL BACKTEST[/b]", markup=True, color=CYAN, font_size=sp(11), size_hint_y=None, height=dp(22) )) # Backtest description backtest_card.add_widget(Label( text="[color=888888]Test bot on 30 days of historical data at 1000x speed[/color]", markup=True, color=GRAY, font_size=sp(9), size_hint_y=None, height=dp(20) )) # Backtest button row backtest_row = BoxLayout(size_hint_y=None, height=dp(40), spacing=dp(8)) self.backtest_btn = StyledButton( text="[b]RUN BACKTEST[/b]", markup=True, bg_color=CYAN, text_color=WHITE, font_size=sp(11), radius=10, size_hint_x=0.6 ) self.backtest_btn.bind(on_press=self.run_high_speed_backtest) backtest_row.add_widget(self.backtest_btn) self.backtest_status_label = Label( text="[color=888888]Ready[/color]", markup=True, color=WHITE, font_size=sp(10), size_hint_x=0.4, halign='center' ) backtest_row.add_widget(self.backtest_status_label) backtest_card.add_widget(backtest_row) # Backtest results label self.backtest_results_label = Label( text="[color=888888]Results: -- trades | Win Rate: --% | P&L: --[/color]", markup=True, color=WHITE, font_size=sp(9), size_hint_y=None, height=dp(24) ) backtest_card.add_widget(self.backtest_results_label) layout.add_widget(backtest_card) # === OPTIMIZATION ENGINE PANEL === opt_card = BorderedCard(size_hint_y=None, height=dp(160)) opt_card.orientation = 'vertical' opt_card.padding = dp(6) opt_card.spacing = dp(4) opt_card.add_widget(Label( text="[b]OPTIMIZATION ENGINE[/b]", markup=True, color=MAGENTA, font_size=sp(11), size_hint_y=None, height=dp(22) )) # Description opt_card.add_widget(Label( text="[color=888888]Run multiple backtests with different parameters to find optimal settings[/color]", markup=True, color=GRAY, font_size=sp(9), size_hint_y=None, height=dp(20) )) # Optimization button row opt_row = BoxLayout(size_hint_y=None, height=dp(40), spacing=dp(8)) self.optimize_btn = StyledButton( text="[b]RUN OPTIMIZATION[/b]", markup=True, bg_color=MAGENTA, text_color=WHITE, font_size=sp(11), radius=10, size_hint_x=0.6 ) self.optimize_btn.bind(on_press=self.run_optimization_suite) opt_row.add_widget(self.optimize_btn) self.optimize_status_label = Label( text="[color=888888]Ready[/color]", markup=True, color=WHITE, font_size=sp(10), size_hint_x=0.4, halign='center' ) opt_row.add_widget(self.optimize_status_label) opt_card.add_widget(opt_row) # Optimization results self.optimize_results_label = Label( text="[color=888888]Best: -- | Iterations: -- | Time: --[/color]", markup=True, color=WHITE, font_size=sp(9), size_hint_y=None, height=dp(24) ) opt_card.add_widget(self.optimize_results_label) # Optimal config display self.optimal_config_label = Label( text="[color=888888]Optimal: Risk=-- | Score=-- | R/R=-- | Trail=--[/color]", markup=True, color=CYAN, font_size=sp(8), size_hint_y=None, height=dp(20) ) opt_card.add_widget(self.optimal_config_label) layout.add_widget(opt_card) # === AUTONOMOUS OPTIMIZATION PANEL === auto_opt_card = BorderedCard(size_hint_y=None, height=dp(240)) auto_opt_card.orientation = 'vertical' auto_opt_card.padding = dp(6) auto_opt_card.spacing = dp(4) auto_opt_card.add_widget(Label( text="[b]SMART OPTIMIZER: FIND & TRADE[/b]", markup=True, color=(0.9, 0.2, 0.7, 1), # Bright magenta font_size=sp(11), size_hint_y=None, height=dp(22) )) # Description auto_opt_card.add_widget(Label( text="[color=888888]Optimizes until targets met, then auto-stops and enables trading.[/color]", markup=True, color=GRAY, font_size=sp(9), size_hint_y=None, height=dp(18) )) # Target Settings Row targets_row = BoxLayout(size_hint_y=None, height=dp(30), spacing=dp(4)) # Min Win Rate input targets_row.add_widget(Label(text="[color=AAAAAA]WR%:[/color]", markup=True, font_size=sp(9), size_hint_x=0.15)) self.target_wr_input = TextInput( text="55", multiline=False, input_filter='int', font_size=sp(10), size_hint_x=0.20, background_color=(0.1, 0.1, 0.12, 1), foreground_color=(1, 1, 1, 1), padding=(4, 4) ) targets_row.add_widget(self.target_wr_input) # Max Drawdown input targets_row.add_widget(Label(text="[color=AAAAAA]DD%:[/color]", markup=True, font_size=sp(9), size_hint_x=0.15)) self.target_dd_input = TextInput( text="10", multiline=False, input_filter='int', font_size=sp(10), size_hint_x=0.20, background_color=(0.1, 0.1, 0.12, 1), foreground_color=(1, 1, 1, 1), padding=(4, 4) ) targets_row.add_widget(self.target_dd_input) # Min Return input targets_row.add_widget(Label(text="[color=AAAAAA]Ret%:[/color]", markup=True, font_size=sp(9), size_hint_x=0.15)) self.target_ret_input = TextInput( text="15", multiline=False, input_filter='int', font_size=sp(10), size_hint_x=0.20, background_color=(0.1, 0.1, 0.12, 1), foreground_color=(1, 1, 1, 1), padding=(4, 4) ) targets_row.add_widget(self.target_ret_input) auto_opt_card.add_widget(targets_row) # Auto-trade checkbox row auto_trade_row = BoxLayout(size_hint_y=None, height=dp(28), spacing=dp(4)) self.auto_enable_trading = True # Default to True self.auto_trade_checkbox = ToggleButton( text="[b]✓ AUTO-ENABLE TRADING[/b]", markup=True, state='down', font_size=sp(9), background_color=(0.2, 0.8, 0.2, 1), size_hint_x=0.5 ) self.auto_trade_checkbox.bind(on_press=self._toggle_auto_trade) auto_trade_row.add_widget(self.auto_trade_checkbox) auto_trade_row.add_widget(Label( text="[color=888888]When optimal, auto-start trading[/color]", markup=True, font_size=sp(8), size_hint_x=0.5 )) auto_opt_card.add_widget(auto_trade_row) # Control buttons row auto_row = BoxLayout(size_hint_y=None, height=dp(40), spacing=dp(8)) self.auto_opt_start_btn = StyledButton( text="[b]FIND & TRADE[/b]", markup=True, bg_color=(0.9, 0.2, 0.7, 1), text_color=WHITE, font_size=sp(11), radius=10, size_hint_x=0.5 ) self.auto_opt_start_btn.bind(on_press=self.start_autonomous_optimization) auto_row.add_widget(self.auto_opt_start_btn) self.auto_opt_stop_btn = StyledButton( text="[b]STOP[/b]", markup=True, bg_color=GRAY, text_color=WHITE, font_size=sp(11), radius=10, size_hint_x=0.25 ) self.auto_opt_stop_btn.bind(on_press=self.stop_autonomous_optimization) auto_row.add_widget(self.auto_opt_stop_btn) auto_row.add_widget(Label(size_hint_x=0.05)) # Spacer self.auto_opt_status_label = Label( text="[color=888888]Ready[/color]", markup=True, color=WHITE, font_size=sp(10), size_hint_x=0.2, halign='center' ) auto_row.add_widget(self.auto_opt_status_label) auto_opt_card.add_widget(auto_row) # Progress bar self.auto_progress_label = Label( text="[color=888888]Progress: 0% to targets | Iteration: 0[/color]", markup=True, color=CYAN, font_size=sp(9), size_hint_y=None, height=dp(18) ) auto_opt_card.add_widget(self.auto_progress_label) # Best result display self.auto_best_label = Label( text="[color=888888]Best: -- | Config: --[/color]", markup=True, color=(0.2, 1.0, 0.2, 1), font_size=sp(8), size_hint_y=None, height=dp(18) ) auto_opt_card.add_widget(self.auto_best_label) layout.add_widget(auto_opt_card) # === CATEGORY 1: SETUP SEARCH (Single Line Layout) === setup_perf_card = BorderedCard(size_hint_y=None, height=dp(45)) setup_perf_card.orientation = 'horizontal' setup_perf_card.padding = dp(4) setup_perf_card.spacing = dp(4) self.setup_label = Label(text="[b]SCAN: SETUP SEARCH[/b]", markup=True, color=BLUE, font_size=sp(10), size_hint_x=0.35, halign='left') self.setup_perf_details = Label(text="[color=888888]-- ms | --% | -- pairs[/color]", markup=True, color=WHITE, font_size=sp(9), size_hint_x=0.45, halign='center') self.setup_grade_label = Label(text="[b]--[/b]", markup=True, color=GRAY, font_size=sp(12), size_hint_x=0.2, halign='right') setup_perf_card.add_widget(self.setup_label) setup_perf_card.add_widget(self.setup_perf_details) setup_perf_card.add_widget(self.setup_grade_label) layout.add_widget(setup_perf_card) # === CATEGORY 2: FILTERING (Single Line Layout) === filter_perf_card = BorderedCard(size_hint_y=None, height=dp(45)) filter_perf_card.orientation = 'horizontal' filter_perf_card.padding = dp(4) filter_perf_card.spacing = dp(4) self.filter_label = Label(text="[b]SPD: FILTERING[/b]", markup=True, color=BLUE, font_size=sp(10), size_hint_x=0.35, halign='left') self.filter_perf_details = Label(text="[color=888888]-- ms | --% eff[/color]", markup=True, color=WHITE, font_size=sp(9), size_hint_x=0.45, halign='center') self.filter_grade_label = Label(text="[b]--[/b]", markup=True, color=GRAY, font_size=sp(12), size_hint_x=0.2, halign='right') filter_perf_card.add_widget(self.filter_label) filter_perf_card.add_widget(self.filter_perf_details) filter_perf_card.add_widget(self.filter_grade_label) layout.add_widget(filter_perf_card) # === CATEGORY 3: TRADE ALGORITHM (Single Line Layout) === trade_perf_card = BorderedCard(size_hint_y=None, height=dp(45)) trade_perf_card.orientation = 'horizontal' trade_perf_card.padding = dp(4) trade_perf_card.spacing = dp(4) self.trade_label = Label(text="[b]TRADE ALGO[/b]", markup=True, color=GREEN, font_size=sp(10), size_hint_x=0.35, halign='left') self.trade_perf_details = Label(text="[color=888888]-- ms | Trail: -- ms[/color]", markup=True, color=WHITE, font_size=sp(9), size_hint_x=0.45, halign='center') self.trade_grade_label = Label(text="[b]--[/b]", markup=True, color=GRAY, font_size=sp(12), size_hint_x=0.2, halign='right') trade_perf_card.add_widget(self.trade_label) trade_perf_card.add_widget(self.trade_perf_details) trade_perf_card.add_widget(self.trade_grade_label) layout.add_widget(trade_perf_card) # === CATEGORY 4: ORDER BOOK (Single Line Layout) === ob_perf_card = BorderedCard(size_hint_y=None, height=dp(45)) ob_perf_card.orientation = 'horizontal' ob_perf_card.padding = dp(4) ob_perf_card.spacing = dp(4) self.ob_label = Label(text="[b]ORDER BOOK[/b]", markup=True, color=AMBER, font_size=sp(10), size_hint_x=0.35, halign='left') self.ob_perf_details = Label(text="[color=888888]-- ms | -- ms[/color]", markup=True, color=WHITE, font_size=sp(9), size_hint_x=0.45, halign='center') self.ob_grade_label = Label(text="[b]--[/b]", markup=True, color=GRAY, font_size=sp(12), size_hint_x=0.2, halign='right') ob_perf_card.add_widget(self.ob_label) ob_perf_card.add_widget(self.ob_perf_details) ob_perf_card.add_widget(self.ob_grade_label) layout.add_widget(ob_perf_card) # === CATEGORY 5: API EFFICIENCY (Single Line Layout) === api_perf_card = BorderedCard(size_hint_y=None, height=dp(45)) api_perf_card.orientation = 'horizontal' api_perf_card.padding = dp(4) api_perf_card.spacing = dp(4) self.api_label = Label(text="[b]API[/b]", markup=True, color=ORANGE, font_size=sp(10), size_hint_x=0.35, halign='left') self.api_perf_details = Label(text="[color=888888]-- ms | -- calls | --% err[/color]", markup=True, color=WHITE, font_size=sp(9), size_hint_x=0.45, halign='center') self.api_grade_label = Label(text="[b]--[/b]", markup=True, color=GRAY, font_size=sp(12), size_hint_x=0.2, halign='right') api_perf_card.add_widget(self.api_label) api_perf_card.add_widget(self.api_perf_details) api_perf_card.add_widget(self.api_grade_label) layout.add_widget(api_perf_card) # === CATEGORY 6: P&L TRACKING (Single Line Layout) === pnl_perf_card = BorderedCard(size_hint_y=None, height=dp(45)) pnl_perf_card.orientation = 'horizontal' pnl_perf_card.padding = dp(4) pnl_perf_card.spacing = dp(4) self.pnl_label = Label(text="[b]P&L[/b]", markup=True, color=GREEN, font_size=sp(10), size_hint_x=0.35, halign='left') self.pnl_perf_details = Label(text="[color=888888]$0 | $0 | $0[/color]", markup=True, color=WHITE, font_size=sp(9), size_hint_x=0.45, halign='center') self.pnl_grade_label = Label(text="[b]--[/b]", markup=True, color=GRAY, font_size=sp(12), size_hint_x=0.2, halign='right') pnl_perf_card.add_widget(self.pnl_label) pnl_perf_card.add_widget(self.pnl_perf_details) pnl_perf_card.add_widget(self.pnl_grade_label) layout.add_widget(pnl_perf_card) # === OPTIMIZATION REPORT BUTTON === report_btn = StyledButton( text="[b]OPTIMIZATION REPORT[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(11), radius=8, size_hint_y=None, height=dp(40) ) report_btn.bind(on_press=self.generate_optimization_report) layout.add_widget(report_btn) # === DIVIDER === layout.add_widget(Label(text="", size_hint_y=None, height=dp(8))) # === TEST BUTTONS === tests_header = EmojiLabel( text="[b]🧪 DIAGNOSTIC TESTS[/b]", markup=True, color=GOLD, font_size=sp(12), size_hint_y=None, height=dp(28) ) layout.add_widget(tests_header) # Test results display self.test_results_label = Label( text="[color=888888]Tap a test button below to run diagnostics...[/color]", markup=True, color=WHITE, font_size=sp(10), size_hint_y=None, height=dp(40), halign='center' ) layout.add_widget(self.test_results_label) # Quick test buttons row quick_tests = BoxLayout(size_hint_y=None, height=dp(45), spacing=dp(6)) btn_quick = StyledButton( text="[b]QUICK CHECK[/b]", markup=True, bg_color=BLUE, text_color=WHITE, font_size=sp(11), radius=8 ) btn_quick.bind(on_press=self.run_quick_check) quick_tests.add_widget(btn_quick) btn_full = StyledButton( text="[b]FULL DIAGNOSTIC[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(11), radius=8 ) btn_full.bind(on_press=self.run_all_tests) quick_tests.add_widget(btn_full) btn_live = StyledButton( text="[b]LIVE READY?[/b]", markup=True, bg_color=AMBER, text_color=BLACK, font_size=sp(11), radius=8 ) btn_live.bind(on_press=self.run_live_readiness_check) quick_tests.add_widget(btn_live) layout.add_widget(quick_tests) # Speed optimization test button (full width) speed_test_btn = StyledButton( text="[b]SPEED OPTIMIZATION TEST[/b]", markup=True, bg_color=PURPLE, text_color=WHITE, font_size=sp(12), radius=10, size_hint_y=None, height=dp(50) ) speed_test_btn.bind(on_press=self.run_comprehensive_speed_test) layout.add_widget(speed_test_btn) # === TEST LOG - CONTAINED IN SCROLLVIEW === log_card = BorderedCard(size_hint_y=None, height=dp(200)) log_card.orientation = 'vertical' log_card.padding = dp(6) log_card.spacing = dp(4) log_card.add_widget(Label(text="[b]TEST LOG[/b]", markup=True, color=GOLD, font_size=sp(12), size_hint_y=None, height=dp(24))) # Contain log in ScrollView to prevent overflow log_scroll = ScrollView(size_hint_y=1) self.test_log_widget = Label( text="[color=888888]No tests run yet...[/color]", markup=True, color=WHITE, font_size=sp(9), size_hint_y=None, halign='left', valign='top', text_size=(380, None) ) self.test_log_widget.bind(texture_size=lambda i, v: setattr(i, 'height', max(v[1], dp(150)))) log_scroll.add_widget(self.test_log_widget) log_card.add_widget(log_scroll) layout.add_widget(log_card) # ============================================ # TEST MODE CONTROLS (Moved from ROB-BOT tab) # ============================================ test_control_card = BorderedCard(size_hint_y=None, height=dp(196)) test_control_card.orientation = 'vertical' test_control_card.add_widget(Label( text="[b]TEST MODE CONTROLS[/b]", markup=True, color=AMBER, font_size=sp(11), size_hint_y=None, height=dp(22) )) # All Limits Off limits_row = BoxLayout(size_hint_y=None, height=dp(36), spacing=dp(8), padding=dp(4)) limits_row.add_widget(Label(text="All Limits Off:", color=WHITE, font_size=sp(10), size_hint_x=0.5)) self.all_limits_btn = StyledButton( text="[b]OFF[/b]", markup=True, bg_color=DARK_RED, text_color=WHITE, font_size=sp(11), radius=8, size_hint_x=0.5 ) self.all_limits_btn.bind(on_press=self.toggle_all_limits) limits_row.add_widget(self.all_limits_btn) test_control_card.add_widget(limits_row) # Silent Mode silent_row = BoxLayout(size_hint_y=None, height=dp(36), spacing=dp(8), padding=dp(4)) silent_row.add_widget(Label(text="Silent Mode:", color=WHITE, font_size=sp(10), size_hint_x=0.5)) self.silent_btn = StyledButton( text="[b]OFF[/b]", markup=True, bg_color=GRAY, text_color=WHITE, font_size=sp(11), radius=8, size_hint_x=0.5 ) self.silent_btn.bind(on_press=self.toggle_silent_mode) silent_row.add_widget(self.silent_btn) test_control_card.add_widget(silent_row) # Pre-Trade Validation validation_row = BoxLayout(size_hint_y=None, height=dp(36), spacing=dp(8), padding=dp(4)) validation_row.add_widget(Label(text="Pre-Trade Validation:", color=WHITE, font_size=sp(10), size_hint_x=0.5)) self.validation_btn = StyledButton( text="[b]ON[/b]", markup=True, bg_color=GREEN, text_color=WHITE, font_size=sp(11), radius=8, size_hint_x=0.5 ) self.validation_btn.bind(on_press=self.toggle_pre_trade_valid