diff --git a/core/orchestrator.py b/core/orchestrator.py index eda1998..78ab729 100644 --- a/core/orchestrator.py +++ b/core/orchestrator.py @@ -220,6 +220,7 @@ class TradingOrchestrator: # Load historical data for models and RL training self._load_historical_data_for_models() + # SINGLE-USE FUNCTION - Called only once in codebase def _initialize_ml_models(self): """Initialize ML models for enhanced trading""" try: @@ -592,6 +593,7 @@ class TradingOrchestrator: logger.error(f"Error in extrema trainer prediction: {e}") return None + # UNUSED FUNCTION - Not called anywhere in codebase def get_memory_usage(self) -> float: return 30.0 # MB @@ -623,6 +625,7 @@ class TradingOrchestrator: logger.error(f"Error in transformer prediction: {e}") return None + # UNUSED FUNCTION - Not called anywhere in codebase def get_memory_usage(self) -> float: return 60.0 # MB estimate for transformer @@ -649,6 +652,7 @@ class TradingOrchestrator: logger.error(f"Error in decision model prediction: {e}") return None + # UNUSED FUNCTION - Not called anywhere in codebase def get_memory_usage(self) -> float: return 40.0 # MB estimate for decision model @@ -666,6 +670,7 @@ class TradingOrchestrator: except Exception as e: logger.error(f"Error initializing ML models: {e}") + # UNUSED FUNCTION - Not called anywhere in codebase def update_model_loss(self, model_name: str, current_loss: float, best_loss: float = None): """Update model loss and potentially best loss""" if model_name in self.model_states: @@ -676,6 +681,7 @@ class TradingOrchestrator: self.model_states[model_name]['best_loss'] = current_loss logger.debug(f"Updated {model_name} loss: current={current_loss:.4f}, best={self.model_states[model_name]['best_loss']:.4f}") + # UNUSED FUNCTION - Not called anywhere in codebase def checkpoint_saved(self, model_name: str, checkpoint_data: Dict[str, Any]): """Callback when a model checkpoint is saved""" if model_name in self.model_states: @@ -689,6 +695,7 @@ class TradingOrchestrator: self.model_states[model_name]['best_loss'] = saved_loss logger.info(f"New best loss for {model_name}: {saved_loss:.4f}") + # UNUSED FUNCTION - Not called anywhere in codebase def get_recent_predictions(self, limit: int = 10) -> List[Dict[str, Any]]: """Get recent predictions from all models for data streaming""" try: @@ -728,6 +735,7 @@ class TradingOrchestrator: logger.debug(f"Error getting recent predictions: {e}") return [] + # UNUSED FUNCTION - Not called anywhere in codebase def _save_orchestrator_state(self): """Save the current state of the orchestrator, including model states.""" state = { @@ -742,6 +750,7 @@ class TradingOrchestrator: json.dump(state, f, indent=4) logger.info(f"Orchestrator state saved to {save_path}") + # UNUSED FUNCTION - Not called anywhere in codebase def _load_orchestrator_state(self): """Load the orchestrator state from a saved file.""" save_path = os.path.join(self.config.paths.get('checkpoint_dir', './models/saved'), 'orchestrator_state.json') @@ -777,6 +786,7 @@ class TradingOrchestrator: self.trade_loop_task = asyncio.create_task(self._trading_decision_loop()) logger.info("Continuous trading loop initiated.") + # UNUSED FUNCTION - Not called anywhere in codebase def _initialize_cob_integration(self): """Initialize COB integration for real-time market microstructure data""" if COB_INTEGRATION_AVAILABLE: @@ -807,12 +817,14 @@ class TradingOrchestrator: else: logger.warning("COB Integration not initialized. Cannot start streaming.") + # UNUSED FUNCTION - Not called anywhere in codebase def _start_cob_matrix_worker(self): """Start a background worker to continuously update COB matrices for models""" if not self.cob_integration: logger.warning("COB Integration not available, cannot start COB matrix worker.") return + # UNUSED FUNCTION - Not called anywhere in codebase def matrix_worker(): logger.info("COB Matrix Worker started.") while self.realtime_processing: @@ -851,6 +863,7 @@ class TradingOrchestrator: matrix_thread = threading.Thread(target=matrix_worker, daemon=True) matrix_thread.start() + # UNUSED FUNCTION - Not called anywhere in codebase def _update_cob_matrix_for_symbol(self, symbol: str): """Updates the COB matrix and features for a specific symbol.""" if not self.cob_integration: @@ -967,6 +980,7 @@ class TradingOrchestrator: logger.error(f"Error generating COB DQN features for {symbol}: {e}") return None + # UNUSED FUNCTION - Not called anywhere in codebase def _on_cob_cnn_features(self, symbol: str, cob_data: Dict): """Callback for when new COB CNN features are available""" if not self.realtime_processing: @@ -984,6 +998,7 @@ class TradingOrchestrator: except Exception as e: logger.error(f"Error in _on_cob_cnn_features for {symbol}: {e}") + # UNUSED FUNCTION - Not called anywhere in codebase def _on_cob_dqn_features(self, symbol: str, cob_data: Dict): """Callback for when new COB DQN features are available""" if not self.realtime_processing: @@ -1001,6 +1016,7 @@ class TradingOrchestrator: except Exception as e: logger.error(f"Error in _on_cob_dqn_features for {symbol}: {e}") + # UNUSED FUNCTION - Not called anywhere in codebase def _on_cob_dashboard_data(self, symbol: str, cob_data: Dict): """Callback for when new COB data is available for the dashboard""" if not self.realtime_processing: @@ -1013,20 +1029,24 @@ class TradingOrchestrator: except Exception as e: logger.error(f"Error in _on_cob_dashboard_data for {symbol}: {e}") + # UNUSED FUNCTION - Not called anywhere in codebase def get_cob_features(self, symbol: str) -> Optional[np.ndarray]: """Get the latest COB features for CNN model""" return self.latest_cob_features.get(symbol) + # UNUSED FUNCTION - Not called anywhere in codebase def get_cob_state(self, symbol: str) -> Optional[np.ndarray]: """Get the latest COB state for DQN model""" return self.latest_cob_state.get(symbol) + # SINGLE-USE FUNCTION - Called only once in codebase def get_cob_snapshot(self, symbol: str) -> Optional[COBSnapshot]: """Get the latest raw COB snapshot for a symbol""" if self.cob_integration: return self.cob_integration.get_latest_cob_snapshot(symbol) return None + # SINGLE-USE FUNCTION - Called only once in codebase def get_cob_feature_matrix(self, symbol: str, sequence_length: int = 60) -> Optional[np.ndarray]: """Get a sequence of COB CNN features for sequence models""" if symbol not in self.cob_feature_history or not self.cob_feature_history[symbol]: @@ -1059,6 +1079,7 @@ class TradingOrchestrator: # Weight normalization removed - handled by ModelManager + # UNUSED FUNCTION - Not called anywhere in codebase def add_decision_callback(self, callback): """Add a callback function to be called when decisions are made""" self.decision_callbacks.append(callback) @@ -1322,6 +1343,7 @@ class TradingOrchestrator: logger.debug(f"Error building RL state for {symbol}: {e}") return None + # SINGLE-USE FUNCTION - Called only once in codebase def _get_cob_state(self, symbol: str) -> Optional[np.ndarray]: """Build COB state vector for COB RL agent""" try: @@ -1478,6 +1500,7 @@ class TradingOrchestrator: logger.error(f"Error creating RL state for {symbol}: {e}") return None + # SINGLE-USE FUNCTION - Called only once in codebase def _combine_predictions(self, symbol: str, price: float, predictions: List[Prediction], timestamp: datetime) -> TradingDecision: @@ -1593,6 +1616,7 @@ class TradingOrchestrator: current_position_pnl=0.0 ) + # SINGLE-USE FUNCTION - Called only once in codebase def _get_timeframe_weight(self, timeframe: str) -> float: """Get importance weight for a timeframe""" # Higher timeframes get more weight in decision making @@ -1605,12 +1629,14 @@ class TradingOrchestrator: # Model performance and weight adaptation removed - handled by ModelManager # Use self.model_manager for all model performance tracking + # UNUSED FUNCTION - Not called anywhere in codebase def get_recent_decisions(self, symbol: str, limit: int = 10) -> List[TradingDecision]: """Get recent decisions for a symbol""" if symbol in self.recent_decisions: return self.recent_decisions[symbol][-limit:] return [] + # UNUSED FUNCTION - Not called anywhere in codebase def get_performance_metrics(self) -> Dict[str, Any]: """Get performance metrics for the orchestrator""" return { @@ -1625,6 +1651,7 @@ class TradingOrchestrator: } } + # UNUSED FUNCTION - Not called anywhere in codebase def get_model_states(self) -> Dict[str, Dict]: """Get current model states with REAL checkpoint data - SSOT for dashboard""" try: @@ -1749,6 +1776,7 @@ class TradingOrchestrator: 'extrema_trainer': {'initial_loss': None, 'current_loss': None, 'best_loss': None, 'checkpoint_loaded': False} } + # SINGLE-USE FUNCTION - Called only once in codebase def _initialize_decision_fusion(self): """Initialize the decision fusion neural network for learning model effectiveness""" try: @@ -1767,6 +1795,7 @@ class TradingOrchestrator: self.fc3 = nn.Linear(hidden_size, 3) # BUY, SELL, HOLD self.dropout = nn.Dropout(0.2) + # UNUSED FUNCTION - Not called anywhere in codebase def forward(self, x): x = torch.relu(self.fc1(x)) x = self.dropout(x) @@ -1781,6 +1810,7 @@ class TradingOrchestrator: logger.warning(f"Decision fusion initialization failed: {e}") self.decision_fusion_enabled = False + # SINGLE-USE FUNCTION - Called only once in codebase def _initialize_enhanced_training_system(self): """Initialize the enhanced real-time training system""" try: @@ -1825,6 +1855,7 @@ class TradingOrchestrator: self.training_enabled = False self.enhanced_training_system = None + # SINGLE-USE FUNCTION - Called only once in codebase def start_enhanced_training(self): """Start the enhanced real-time training system""" try: @@ -1845,6 +1876,7 @@ class TradingOrchestrator: logger.error(f"Error starting enhanced training: {e}") return False + # UNUSED FUNCTION - Not called anywhere in codebase def stop_enhanced_training(self): """Stop the enhanced real-time training system""" try: @@ -1858,6 +1890,7 @@ class TradingOrchestrator: logger.error(f"Error stopping enhanced training: {e}") return False + # UNUSED FUNCTION - Not called anywhere in codebase def get_enhanced_training_stats(self) -> Dict[str, Any]: """Get enhanced training system statistics with orchestrator integration""" try: @@ -1954,6 +1987,7 @@ class TradingOrchestrator: 'error': str(e) } + # UNUSED FUNCTION - Not called anywhere in codebase def set_training_dashboard(self, dashboard): """Set the dashboard reference for the training system""" try: @@ -1972,6 +2006,7 @@ class TradingOrchestrator: logger.error(f"Error getting universal data stream: {e}") return None + # UNUSED FUNCTION - Not called anywhere in codebase def get_universal_data_for_model(self, model_type: str = 'cnn') -> Optional[Dict[str, Any]]: """Get formatted universal data for specific model types""" try: @@ -2014,6 +2049,7 @@ class TradingOrchestrator: except Exception: return False + # SINGLE-USE FUNCTION - Called only once in codebase def _calculate_aggressiveness_thresholds(self, current_pnl: float, symbol: str) -> tuple: """Calculate confidence thresholds based on aggressiveness settings""" # Base thresholds @@ -2036,6 +2072,7 @@ class TradingOrchestrator: return entry_threshold, exit_threshold + # SINGLE-USE FUNCTION - Called only once in codebase def _apply_pnl_feedback(self, action: str, confidence: float, current_pnl: float, symbol: str, reasoning: dict) -> tuple: """Apply P&L-based feedback to decision making""" @@ -2069,6 +2106,7 @@ class TradingOrchestrator: logger.debug(f"Error applying P&L feedback: {e}") return action, confidence + # SINGLE-USE FUNCTION - Called only once in codebase def _calculate_dynamic_entry_aggressiveness(self, symbol: str) -> float: """Calculate dynamic entry aggressiveness based on recent performance""" try: @@ -2097,6 +2135,7 @@ class TradingOrchestrator: logger.debug(f"Error calculating dynamic entry aggressiveness: {e}") return 0.5 + # SINGLE-USE FUNCTION - Called only once in codebase def _calculate_dynamic_exit_aggressiveness(self, symbol: str, current_pnl: float) -> float: """Calculate dynamic exit aggressiveness based on P&L and market conditions""" try: @@ -2119,11 +2158,13 @@ class TradingOrchestrator: logger.debug(f"Error calculating dynamic exit aggressiveness: {e}") return 0.5 + # UNUSED FUNCTION - Not called anywhere in codebase def set_trading_executor(self, trading_executor): """Set the trading executor for position tracking""" self.trading_executor = trading_executor logger.info("Trading executor set for position tracking and P&L feedback") + # SINGLE-USE FUNCTION - Called only once in codebase def _get_current_price(self, symbol: str) -> float: """Get current price for symbol""" try: @@ -2169,6 +2210,7 @@ class TradingOrchestrator: else: return 1000.0 + # SINGLE-USE FUNCTION - Called only once in codebase def _generate_fallback_prediction(self, symbol: str) -> Dict[str, Any]: """Generate fallback prediction when models fail""" try: @@ -2189,6 +2231,7 @@ class TradingOrchestrator: 'model': 'fallback' } + # UNUSED FUNCTION - Not called anywhere in codebase def capture_dqn_prediction(self, symbol: str, action_idx: int, confidence: float, price: float, q_values: List[float] = None): """Capture DQN prediction for dashboard visualization""" try: @@ -2205,6 +2248,7 @@ class TradingOrchestrator: except Exception as e: logger.debug(f"Error capturing DQN prediction: {e}") + # UNUSED FUNCTION - Not called anywhere in codebase def capture_cnn_prediction(self, symbol: str, direction: int, confidence: float, current_price: float, predicted_price: float): """Capture CNN prediction for dashboard visualization""" try: @@ -2270,6 +2314,7 @@ class TradingOrchestrator: logger.warning(f"Data stream monitor initialization failed: {e}") self.data_stream_monitor = None + # UNUSED FUNCTION - Not called anywhere in codebase def start_data_stream(self) -> bool: """Start data streaming if not already active.""" try: @@ -2282,6 +2327,7 @@ class TradingOrchestrator: logger.error(f"Failed to start data stream: {e}") return False + # UNUSED FUNCTION - Not called anywhere in codebase def stop_data_stream(self) -> bool: """Stop data streaming if active.""" try: @@ -2292,6 +2338,7 @@ class TradingOrchestrator: logger.error(f"Failed to stop data stream: {e}") return False + # SINGLE-USE FUNCTION - Called only once in codebase def get_data_stream_status(self) -> Dict[str, any]: """Return current data stream status and buffer sizes.""" status = { @@ -2310,6 +2357,7 @@ class TradingOrchestrator: pass return status + # UNUSED FUNCTION - Not called anywhere in codebase def save_data_snapshot(self, filepath: str = None) -> str: """Save a snapshot of current data stream buffers to a file. @@ -2337,6 +2385,7 @@ class TradingOrchestrator: logger.error(f"Failed to save data snapshot: {e}") raise + # UNUSED FUNCTION - Not called anywhere in codebase def get_stream_summary(self) -> Dict[str, any]: """Get a summary of current data stream activity.""" status = self.get_data_stream_status() @@ -2360,6 +2409,7 @@ class TradingOrchestrator: return summary + # UNUSED FUNCTION - Not called anywhere in codebase def get_cob_data(self, symbol: str, limit: int = 300) -> List: """Get COB data for a symbol with specified limit.""" try: @@ -2370,6 +2420,7 @@ class TradingOrchestrator: logger.error(f"Error getting COB data: {e}") return [] + # SINGLE-USE FUNCTION - Called only once in codebase def _load_historical_data_for_models(self): """Load 300 historical candles for all required timeframes and symbols for model training""" logger.info("Loading 300 historical candles for model training and RL context...") @@ -2425,6 +2476,7 @@ class TradingOrchestrator: except Exception as e: logger.error(f"Error in historical data loading: {e}") + # SINGLE-USE FUNCTION - Called only once in codebase def _initialize_models_with_historical_data(self, symbols_timeframes: List[Tuple[str, str]]): """Initialize all NN models with historical data using data provider's normalized methods""" try: @@ -2458,6 +2510,7 @@ class TradingOrchestrator: except Exception as e: logger.error(f"Error initializing models with historical data: {e}") + # SINGLE-USE FUNCTION - Called only once in codebase def _initialize_cnn_with_provider_data(self): """Initialize CNN using data provider's normalized feature extraction""" try: @@ -2488,6 +2541,7 @@ class TradingOrchestrator: except Exception as e: logger.error(f"Error initializing CNN with provider data: {e}") + # SINGLE-USE FUNCTION - Called only once in codebase def _initialize_dqn_with_provider_data(self, symbols_timeframes: List[Tuple[str, str]]): """Initialize DQN using data provider's normalized state vector creation""" try: @@ -2505,6 +2559,7 @@ class TradingOrchestrator: except Exception as e: logger.error(f"Error initializing DQN with provider data: {e}") + # SINGLE-USE FUNCTION - Called only once in codebase def _initialize_transformer_with_provider_data(self, symbols_timeframes: List[Tuple[str, str]]): """Initialize Transformer using data provider's normalized sequence creation""" try: @@ -2522,6 +2577,7 @@ class TradingOrchestrator: except Exception as e: logger.error(f"Error initializing Transformer with provider data: {e}") + # SINGLE-USE FUNCTION - Called only once in codebase def _initialize_decision_with_provider_data(self, symbol_features: Dict[str, Dict[str, pd.DataFrame]]): """Initialize Decision Fusion using data provider's feature aggregation""" try: @@ -2551,6 +2607,7 @@ class TradingOrchestrator: except Exception as e: logger.error(f"Error initializing Decision Fusion with provider data: {e}") + # UNUSED FUNCTION - Not called anywhere in codebase def get_ohlcv_data(self, symbol: str, timeframe: str, limit: int = 300) -> List: """Get OHLCV data for a symbol with specified timeframe and limit.""" try: diff --git a/web/clean_dashboard.py b/web/clean_dashboard.py index 76826c9..8b63f43 100644 --- a/web/clean_dashboard.py +++ b/web/clean_dashboard.py @@ -166,8 +166,14 @@ class CleanTradingDashboard: self.cob_update_count = 0 self.last_cob_broadcast: dict = {} # Rate limiting for UI updates self.cob_data_history: Dict[str, deque] = { - 'ETH/USDT': deque(maxlen=61), # Store ~60 seconds of 1s snapshots - 'BTC/USDT': deque(maxlen=61) + 'ETH/USDT': deque(maxlen=120), # Store ~120 seconds of 1s snapshots for MA calculations + 'BTC/USDT': deque(maxlen=120) + } + + # COB imbalance moving averages for different timeframes + self.cob_imbalance_ma: Dict[str, Dict[str, float]] = { + 'ETH/USDT': {}, + 'BTC/USDT': {} } # Initialize timezone @@ -314,9 +320,14 @@ class CleanTradingDashboard: # Get COB data from orchestrator cob_data = self._get_cob_data_with_buckets(symbol, limit) + + # Add COB imbalance moving averages + cob_imbalance_mas = self.cob_imbalance_ma.get(symbol, {}) + return jsonify({ 'symbol': symbol, 'data': cob_data, + 'cob_imbalance_ma': cob_imbalance_mas, 'timestamp': datetime.now().isoformat() }) except Exception as e: @@ -1183,8 +1194,12 @@ class CleanTradingDashboard: # Determine COB data source mode cob_mode = self._get_cob_mode() - eth_components = self.component_manager.format_cob_data(eth_snapshot, 'ETH/USDT', eth_imbalance_stats, cob_mode) - btc_components = self.component_manager.format_cob_data(btc_snapshot, 'BTC/USDT', btc_imbalance_stats, cob_mode) + # Get COB imbalance moving averages + eth_ma_data = self.cob_imbalance_ma.get('ETH/USDT', {}) + btc_ma_data = self.cob_imbalance_ma.get('BTC/USDT', {}) + + eth_components = self.component_manager.format_cob_data(eth_snapshot, 'ETH/USDT', eth_imbalance_stats, cob_mode, eth_ma_data) + btc_components = self.component_manager.format_cob_data(btc_snapshot, 'BTC/USDT', btc_imbalance_stats, cob_mode, btc_ma_data) return eth_components, btc_components @@ -5170,10 +5185,11 @@ class CleanTradingDashboard: } } - # Store in history (keep last 15 seconds) + # Store in history (keep last 120 seconds for MA calculations) self.cob_data_history[symbol].append(cob_snapshot) - if len(self.cob_data_history[symbol]) > 15: # Keep 15 seconds - self.cob_data_history[symbol] = self.cob_data_history[symbol][-15:] + + # Calculate COB imbalance moving averages for different timeframes + self._calculate_cob_imbalance_mas(symbol) # Update latest data self.latest_cob_data[symbol] = cob_snapshot @@ -5189,7 +5205,41 @@ class CleanTradingDashboard: except Exception as e: logger.debug(f"Error collecting COB data for {symbol}: {e}") - + + def _calculate_cob_imbalance_mas(self, symbol: str): + """Calculate COB imbalance moving averages for different timeframes""" + try: + history = self.cob_data_history[symbol] + if len(history) < 2: + return + + # Extract imbalance values from history + imbalances = [snapshot['stats']['imbalance'] for snapshot in history if 'stats' in snapshot and 'imbalance' in snapshot['stats']] + + if not imbalances: + return + + # Calculate moving averages for different timeframes + timeframes = { + '10s': min(10, len(imbalances)), # 10 second MA + '30s': min(30, len(imbalances)), # 30 second MA + '60s': min(60, len(imbalances)), # 60 second MA + } + + for timeframe, periods in timeframes.items(): + if len(imbalances) >= periods: + # Calculate simple moving average + ma_value = sum(imbalances[-periods:]) / periods + self.cob_imbalance_ma[symbol][timeframe] = ma_value + else: + # If not enough data, use current imbalance + self.cob_imbalance_ma[symbol][timeframe] = imbalances[-1] + + logger.debug(f"COB imbalance MAs for {symbol}: {self.cob_imbalance_ma[symbol]}") + + except Exception as e: + logger.debug(f"Error calculating COB imbalance MAs for {symbol}: {e}") + def _generate_bucketed_cob_data(self, symbol: str, cob_snapshot: dict): """Generate bucketed COB data for model feeding""" try: diff --git a/web/component_manager.py b/web/component_manager.py index 68e0361..0a1e4ae 100644 --- a/web/component_manager.py +++ b/web/component_manager.py @@ -272,7 +272,7 @@ class DashboardComponentManager: logger.error(f"Error formatting system status: {e}") return [html.P(f"Error: {str(e)}", className="text-danger small")] - def format_cob_data(self, cob_snapshot, symbol, cumulative_imbalance_stats=None, cob_mode="Unknown"): + def format_cob_data(self, cob_snapshot, symbol, cumulative_imbalance_stats=None, cob_mode="Unknown", imbalance_ma_data=None): """Format COB data into a split view with summary, imbalance stats, and a compact ladder.""" try: if not cob_snapshot: @@ -317,7 +317,7 @@ class DashboardComponentManager: } # --- Left Panel: Overview and Stats --- - overview_panel = self._create_cob_overview_panel(symbol, stats, cumulative_imbalance_stats, cob_mode) + overview_panel = self._create_cob_overview_panel(symbol, stats, cumulative_imbalance_stats, cob_mode, imbalance_ma_data) # --- Right Panel: Compact Ladder --- ladder_panel = self._create_cob_ladder_panel(bids, asks, mid_price, symbol) @@ -331,7 +331,7 @@ class DashboardComponentManager: logger.error(f"Error formatting split COB data: {e}") return html.P(f"Error: {str(e)}", className="text-danger small") - def _create_cob_overview_panel(self, symbol, stats, cumulative_imbalance_stats, cob_mode="Unknown"): + def _create_cob_overview_panel(self, symbol, stats, cumulative_imbalance_stats, cob_mode="Unknown", imbalance_ma_data=None): """Creates the left panel with summary and imbalance stats.""" mid_price = stats.get('mid_price', 0) spread_bps = stats.get('spread_bps', 0) @@ -373,6 +373,20 @@ class DashboardComponentManager: html.Div(imbalance_stats_display), + # COB Imbalance Moving Averages + ma_display = [] + if imbalance_ma_data: + ma_display.append(html.H6("Imbalance MAs", className="mt-3 mb-2 small text-muted text-uppercase")) + for timeframe, ma_value in imbalance_ma_data.items(): + ma_color = "text-success" if ma_value > 0 else "text-danger" + ma_text = f"MA {timeframe}: {ma_value:.3f}" + ma_display.append(html.Div([ + html.Strong(f"{timeframe}: ", className="small"), + html.Span(ma_text, className=f"small {ma_color}") + ], className="mb-1")) + + html.Div(ma_display), + html.Hr(className="my-2"), html.Table([