Spaces:
Sleeping
Sleeping
| """ | |
| APR chart implementations. | |
| """ | |
| import plotly.graph_objects as go | |
| import pandas as pd | |
| import logging | |
| from datetime import datetime | |
| from typing import Dict, Any, Optional, Tuple | |
| from ..config.constants import DATE_RANGES, Y_AXIS_RANGES, FILE_PATHS | |
| from .base_chart import BaseChart | |
| logger = logging.getLogger(__name__) | |
| class APRChart(BaseChart): | |
| """Chart for APR visualizations.""" | |
| def create_chart(self, df: pd.DataFrame, **kwargs) -> go.Figure: | |
| """Create APR time series chart.""" | |
| if df.empty: | |
| return self._create_empty_chart("No APR data available") | |
| # Filter for APR data only | |
| apr_data = df[df['metric_type'] == 'APR'].copy() | |
| if apr_data.empty: | |
| return self._create_empty_chart("No APR data available") | |
| # Apply daily median aggregation to reduce outliers | |
| apr_data = self.data_processor.aggregate_daily_medians(apr_data, ['apr', 'adjusted_apr']) | |
| if apr_data.empty: | |
| return self._create_empty_chart("No APR data available after aggregation") | |
| # Apply high APR filtering (400% threshold) with forward filling | |
| apr_data, _ = self.data_processor.filter_high_apr_values(apr_data) | |
| if apr_data.empty: | |
| return self._create_empty_chart("No APR data available after high APR filtering") | |
| # Save processed APR data for verification (after all processing steps) | |
| processed_csv_path = self.data_processor.save_to_csv(apr_data, FILE_PATHS['apr_processed_csv']) | |
| if processed_csv_path: | |
| logger.info(f"Saved processed APR data to {processed_csv_path} for verification") | |
| logger.info(f"Processed APR data contains {len(apr_data)} rows after agent exclusion, zero filtering, daily median aggregation, and high APR filtering") | |
| # Filter outliers (disabled but keeping for compatibility) | |
| apr_data = self._filter_outliers(apr_data, 'apr') | |
| # Get time range | |
| min_time = apr_data['timestamp'].min() | |
| max_time = apr_data['timestamp'].max() | |
| x_start_date = DATE_RANGES['apr_start'] | |
| # Create figure | |
| fig = self._create_base_figure() | |
| # Add background shapes | |
| y_range = Y_AXIS_RANGES['apr'] | |
| self._add_background_shapes(fig, min_time, max_time, y_range['min'], y_range['max']) | |
| self._add_zero_line(fig, min_time, max_time) | |
| # Calculate moving averages | |
| avg_apr_data = self._calculate_moving_average(apr_data, 'apr') | |
| # Add individual agent data points | |
| unique_agents = apr_data['agent_name'].unique() | |
| color_map = self._get_color_map(unique_agents) | |
| self._add_agent_data_points(fig, apr_data, 'apr', color_map) | |
| # Add APR moving average line | |
| self._add_moving_average_line( | |
| fig, avg_apr_data, 'apr', | |
| 'Average APR (7d window)', | |
| self.colors['apr'], | |
| width=3 | |
| ) | |
| # Add adjusted APR moving average if available | |
| if 'adjusted_apr' in apr_data.columns and apr_data['adjusted_apr'].notna().any(): | |
| # Calculate adjusted APR moving average | |
| adjusted_avg_data = self._calculate_moving_average(apr_data, 'adjusted_apr') | |
| # Handle missing values with forward fill (fix the column name) | |
| import warnings | |
| with warnings.catch_warnings(): | |
| warnings.simplefilter("ignore", FutureWarning) | |
| # Fix: Use the correct column name 'moving_avg' not 'adjusted_moving_avg' | |
| adjusted_avg_data['moving_avg'] = adjusted_avg_data['moving_avg'].ffill() | |
| self._add_moving_average_line( | |
| fig, adjusted_avg_data, 'adjusted_apr', | |
| 'Average ETH Adjusted APR (7d window)', | |
| self.colors['adjusted_apr'], | |
| width=3 | |
| ) | |
| # Update layout and axes | |
| self._update_layout( | |
| fig, | |
| title="Modius Agents", | |
| y_axis_title=None, # Remove single y-axis title to use region-specific labels | |
| y_range=[y_range['min'], y_range['max']] | |
| ) | |
| # Use auto-range for both x-axis and y-axis to show all available data | |
| self._update_axes( | |
| fig, | |
| x_range=None, # Let plotly auto-determine the best x-axis range | |
| y_auto=True # Let plotly auto-determine the best y-axis range | |
| ) | |
| # Add region-specific annotations for positive and negative areas | |
| self._add_region_annotations(fig, y_range) | |
| # Save chart | |
| self._save_chart( | |
| fig, | |
| FILE_PATHS['apr_graph_html'], | |
| FILE_PATHS['apr_graph_png'] | |
| ) | |
| return fig | |
| def _add_region_annotations(self, fig: go.Figure, y_range: Dict[str, float]) -> None: | |
| """Add annotations for positive and negative regions.""" | |
| # Annotation for negative region - positioned lower to avoid overlap | |
| fig.add_annotation( | |
| x=-0.08, | |
| y=-50, | |
| xref="paper", | |
| yref="y", | |
| text="Percent drawdown [%]", | |
| showarrow=False, | |
| font=dict( | |
| size=14, | |
| family=self.config['font_family'], | |
| color="black", | |
| weight="bold" | |
| ), | |
| textangle=-90, | |
| align="center" | |
| ) | |
| # Annotation for positive region - positioned higher to avoid overlap | |
| fig.add_annotation( | |
| x=-0.08, | |
| y=100, | |
| xref="paper", | |
| yref="y", | |
| text="Agent APR [%]", | |
| showarrow=False, | |
| font=dict( | |
| size=14, | |
| family=self.config['font_family'], | |
| color="black", | |
| weight="bold" | |
| ), | |
| textangle=-90, | |
| align="center" | |
| ) | |
| class APRHashChart(BaseChart): | |
| """Chart for APR vs Agent Hash visualizations.""" | |
| def create_chart(self, df: pd.DataFrame, **kwargs) -> go.Figure: | |
| """Create APR vs agent hash bar chart.""" | |
| if df.empty: | |
| return self._create_empty_chart("No agent hash data available") | |
| # Data is already filtered and processed by the calling function | |
| # Just filter for data with agent hash | |
| apr_data = df[df['agent_hash'].notna()].copy() | |
| if apr_data.empty: | |
| return self._create_empty_chart("No valid APR data with agent_hash found") | |
| logger.info(f"APR Hash Chart: Using {len(apr_data)} processed data points") | |
| # Create figure | |
| fig = self._create_base_figure() | |
| # Get unique hashes and create version mapping | |
| unique_hashes = apr_data['agent_hash'].unique() | |
| version_map = self._create_version_map(unique_hashes) | |
| # Sort hashes by version | |
| sorted_hashes = sorted(unique_hashes, key=lambda h: "1" if h.endswith("tby") else "2" if h.endswith("vq") else h) | |
| # Add zero line for bar chart | |
| self._add_zero_line(fig, -0.5, len(version_map) - 0.5) | |
| # Version colors | |
| version_colors = { | |
| "v0.4.1": "rgba(31, 119, 180, 0.7)", | |
| "v0.4.2": "rgba(44, 160, 44, 0.7)", | |
| } | |
| default_color = "rgba(214, 39, 40, 0.7)" | |
| # Aggregate data by version for bar chart | |
| version_data = {} | |
| version_stats = {} | |
| # Aggregate data by version | |
| for agent_hash in sorted_hashes: | |
| hash_data = apr_data[apr_data['agent_hash'] == agent_hash] | |
| version = version_map[agent_hash] | |
| # Calculate statistics | |
| apr_values = hash_data['apr'].tolist() | |
| # Store statistics for version comparison | |
| if version not in version_stats: | |
| version_stats[version] = {'apr_values': [], 'count': 0, 'hashes': []} | |
| version_stats[version]['apr_values'].extend(apr_values) | |
| version_stats[version]['count'] += len(apr_values) | |
| version_stats[version]['hashes'].append(agent_hash) | |
| # Create bar chart data | |
| versions = list(version_stats.keys()) | |
| medians = [] | |
| colors = [] | |
| hover_texts = [] | |
| for version in versions: | |
| # Calculate median APR for this version | |
| all_values = version_stats[version]['apr_values'] | |
| median_apr = pd.Series(all_values).median() | |
| medians.append(median_apr) | |
| # Choose color | |
| color = version_colors.get(version, default_color) | |
| colors.append(color) | |
| # Create hover text | |
| count = version_stats[version]['count'] | |
| mean_apr = pd.Series(all_values).mean() | |
| min_apr = pd.Series(all_values).min() | |
| max_apr = pd.Series(all_values).max() | |
| hover_text = ( | |
| f"Version: {version}<br>" | |
| f"Median APR: {median_apr:.2f}%<br>" | |
| f"Mean APR: {mean_apr:.2f}%<br>" | |
| f"Min APR: {min_apr:.2f}%<br>" | |
| f"Max APR: {max_apr:.2f}%<br>" | |
| f"Data points: {count}" | |
| ) | |
| hover_texts.append(hover_text) | |
| # Add bar chart | |
| fig.add_trace( | |
| go.Bar( | |
| x=versions, | |
| y=medians, | |
| marker=dict( | |
| color=colors, | |
| line=dict(width=1, color='black') | |
| ), | |
| hoverinfo='text', | |
| hovertext=hover_texts, | |
| showlegend=False, | |
| name="Median APR" | |
| ) | |
| ) | |
| # Add median value annotations on top of bars | |
| for i, (version, median_apr) in enumerate(zip(versions, medians)): | |
| fig.add_annotation( | |
| x=version, | |
| y=median_apr + (max(medians) * 0.05), # 5% above the bar | |
| text=f"{median_apr:.1f}%", | |
| showarrow=False, | |
| font=dict( | |
| family=self.config['font_family'], | |
| size=14, | |
| color="black", | |
| weight="bold" | |
| ) | |
| ) | |
| # Add version comparison annotation | |
| self._add_version_comparison(fig, version_stats, len(versions)) | |
| # Update layout | |
| self._update_layout( | |
| fig, | |
| title="Performance Graph", | |
| height=900 | |
| ) | |
| # Update axes | |
| self._update_axes(fig, y_auto=True) | |
| # Update x-axis for bar chart | |
| fig.update_xaxes( | |
| tickangle=-45 | |
| ) | |
| # Save chart | |
| self._save_chart( | |
| fig, | |
| FILE_PATHS['apr_hash_graph_html'], | |
| FILE_PATHS['apr_hash_graph_png'] | |
| ) | |
| return fig | |
| def _create_version_map(self, hashes: list) -> Dict[str, str]: | |
| """Create version mapping for agent hashes.""" | |
| version_map = {} | |
| for hash_val in hashes: | |
| if hash_val.endswith("tby"): | |
| version_map[hash_val] = "v0.4.1" | |
| elif hash_val.endswith("vq"): | |
| version_map[hash_val] = "v0.4.2" | |
| else: | |
| version_map[hash_val] = f"Hash: {hash_val[-6:]}" | |
| return version_map | |
| def _add_version_comparison(self, fig: go.Figure, version_stats: Dict, num_hashes: int) -> None: | |
| """Add version comparison annotation.""" | |
| if "v0.4.1" in version_stats and "v0.4.2" in version_stats: | |
| v041_values = version_stats["v0.4.1"]["apr_values"] | |
| v042_values = version_stats["v0.4.2"]["apr_values"] | |
| v041_median = pd.Series(v041_values).median() | |
| v042_median = pd.Series(v042_values).median() | |
| improvement = v042_median - v041_median | |
| change_text = "improvement" if improvement > 0 else "decrease" | |
| fig.add_annotation( | |
| x=(num_hashes - 1) / 2, | |
| y=90, | |
| text=f"<b>Version Comparison:</b> {abs(improvement):.2f}% {change_text} from v0.4.1 to v0.4.2", | |
| showarrow=False, | |
| font=dict( | |
| family=self.config['font_family'], | |
| size=16, | |
| color="black", | |
| weight="bold" | |
| ), | |
| bgcolor="rgba(255, 255, 255, 0.9)", | |
| bordercolor="black", | |
| borderwidth=2, | |
| borderpad=6, | |
| opacity=0.9 | |
| ) | |
| def generate_apr_visualizations(data_processor=None) -> Tuple[go.Figure, Optional[str]]: | |
| """Generate APR visualizations.""" | |
| from ..data.data_processor import DataProcessor | |
| if data_processor is None: | |
| data_processor = DataProcessor() | |
| # Fetch data | |
| apr_df, _ = data_processor.fetch_apr_data_from_db() | |
| # Create chart | |
| apr_chart = APRChart(data_processor) | |
| fig, csv_path = apr_chart.generate_visualization( | |
| apr_df, | |
| csv_filename=FILE_PATHS['apr_csv'] | |
| ) | |
| return fig, csv_path | |
| def generate_apr_vs_agent_hash_visualizations(df: pd.DataFrame) -> Tuple[go.Figure, Optional[str]]: | |
| """Generate APR vs agent hash visualizations.""" | |
| from ..data.data_processor import DataProcessor | |
| data_processor = DataProcessor() | |
| # Use the same processed data as the APR time series chart | |
| # First apply the same filtering pipeline to get consistent data | |
| apr_data = df[df['metric_type'] == 'APR'].copy() | |
| if not apr_data.empty: | |
| # Apply daily median aggregation | |
| apr_data = data_processor.aggregate_daily_medians(apr_data, ['apr', 'adjusted_apr']) | |
| # Apply high APR filtering | |
| apr_data, _ = data_processor.filter_high_apr_values(apr_data) | |
| # Create chart using the processed data | |
| apr_hash_chart = APRHashChart(data_processor) | |
| fig, csv_path = apr_hash_chart.generate_visualization( | |
| apr_data, | |
| csv_filename=FILE_PATHS['apr_hash_csv'] | |
| ) | |
| return fig, csv_path | |