""" Utility functions for the Trading Agents Dashboard. This module provides helper functions for loading data from various sources including statistics, recommendations, positions, and quick metrics. """ import json from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union from tradingagents.utils.logger import get_logger logger = get_logger(__name__) def get_data_directory() -> Path: """Get the data directory path.""" return Path(__file__).parent.parent.parent / "data" def load_statistics() -> Dict[str, Any]: """ Load statistics data from JSON file. Returns: Dictionary containing statistics data """ stats_file = get_data_directory() / "recommendations" / "statistics.json" if not stats_file.exists(): return {} try: with open(stats_file, "r") as f: return json.load(f) except Exception as e: logger.error(f"Error loading statistics: {e}") return {} def _extract_date_from_filename(filename: str) -> Optional[str]: name = filename if name.endswith("_recommendations.json"): date_str = name[: -len("_recommendations.json")] elif name.endswith(".json"): date_str = name[:-5] else: return None try: datetime.strptime(date_str, "%Y-%m-%d") return date_str except ValueError: return None def _find_latest_recommendations_file( recommendations_dir: Path, ) -> Tuple[Optional[Path], Optional[str]]: if not recommendations_dir.exists(): return None, None ignore = {"statistics.json", "performance_database.json"} dated_files: List[Tuple[str, Path]] = [] for path in recommendations_dir.glob("*.json"): if path.name in ignore: continue date_str = _extract_date_from_filename(path.name) if date_str: dated_files.append((date_str, path)) if not dated_files: return None, None dated_files.sort(key=lambda item: item[0]) latest_date, latest_path = dated_files[-1] return latest_path, latest_date def _load_recommendations_payload( rec_file: Path, ) -> Tuple[List[Dict[str, Any]], Optional[str]]: try: with open(rec_file, "r") as f: data = json.load(f) except Exception as e: logger.error(f"Error loading recommendations from {rec_file}: {e}") return [], None if isinstance(data, dict): return data.get("recommendations", []) or [], data.get("date") if isinstance(data, list): return data, None return [], None def load_recommendations( date: Optional[str] = None, *, return_meta: bool = False ) -> Union[List[Dict[str, Any]], Tuple[List[Dict[str, Any]], Dict[str, Any]]]: """ Load recommendations data from JSON file. Args: date: Optional date in format YYYY-MM-DD. If None, loads today's recommendations. return_meta: If True, returns (recommendations, meta) tuple. Returns: List of recommendation dictionaries """ requested_date = date or datetime.now().strftime("%Y-%m-%d") recommendations_dir = get_data_directory() / "recommendations" candidates = [ recommendations_dir / f"{requested_date}_recommendations.json", recommendations_dir / f"{requested_date}.json", ] rec_file = next((p for p in candidates if p.exists()), None) used_date = requested_date is_fallback = False if rec_file is None and date is None: rec_file, latest_date = _find_latest_recommendations_file(recommendations_dir) if rec_file is not None: used_date = latest_date or requested_date is_fallback = True if rec_file is None: meta = { "requested_date": requested_date, "date": None, "source_file": None, "is_fallback": False, } return ([], meta) if return_meta else [] recommendations, payload_date = _load_recommendations_payload(rec_file) if payload_date: used_date = payload_date meta = { "requested_date": requested_date, "date": used_date, "source_file": str(rec_file), "is_fallback": is_fallback, } return (recommendations, meta) if return_meta else recommendations def load_open_positions() -> List[Dict[str, Any]]: """ Load open positions from the position tracker. Returns: List of open position dictionaries """ try: from tradingagents.dataflows.discovery.performance.position_tracker import PositionTracker tracker = PositionTracker() positions = tracker.load_all_open_positions() return positions if positions else [] except Exception as e: logger.error(f"Error loading open positions: {e}") return [] def load_performance_database() -> List[Dict[str, Any]]: """ Load the performance database (flattened list of recommendations). """ db_file = get_data_directory() / "recommendations" / "performance_database.json" if not db_file.exists(): return [] try: with open(db_file, "r") as f: data = json.load(f) except Exception as e: logger.error(f"Error loading performance database: {e}") return [] if isinstance(data, dict): by_date = data.get("recommendations_by_date", {}) recs: List[Dict[str, Any]] = [] for items in by_date.values(): if isinstance(items, list): recs.extend(items) return recs if isinstance(data, list): return data return [] _STRATEGY_ALIASES: Dict[str, str] = { "momentum": "momentum", "momentum/hype": "momentum", "momentum/hype / short squeeze": "momentum", "insider play": "insider_buying", "insider_buying": "insider_buying", "earnings play": "earnings_play", "earnings_play": "earnings_play", "earnings_calendar": "earnings_calendar", "news catalyst": "news_catalyst", "news_catalyst": "news_catalyst", "volume accumulation": "volume_accumulation", "volume_accumulation": "volume_accumulation", "contrarian value": "contrarian_value", "contrarian_value": "contrarian_value", } def normalize_strategy(raw: str) -> str: """Map strategy name variants to a canonical lowercase form.""" key = raw.strip().lower() return _STRATEGY_ALIASES.get(key, key) def load_strategy_metrics() -> List[Dict[str, Any]]: """ Build per-strategy metrics from the performance database if available. Falls back to statistics.json when performance database is missing. Normalizes strategy names so variants like 'Momentum', 'momentum', and 'Momentum/Hype' all merge into a single bucket. Counts ALL recommendations per strategy; win rate and avg return are computed from the subset that has 7-day return data. """ recs = load_performance_database() if recs: metrics: Dict[str, Dict[str, float]] = {} for rec in recs: strategy = normalize_strategy(rec.get("strategy_match", "unknown")) if strategy not in metrics: metrics[strategy] = { "total": 0, "evaluated": 0, "wins": 0, "sum_return": 0.0, } metrics[strategy]["total"] += 1 if "return_7d" in rec and rec["return_7d"] is not None: metrics[strategy]["evaluated"] += 1 metrics[strategy]["sum_return"] += float(rec.get("return_7d", 0.0) or 0.0) if rec.get("win_7d"): metrics[strategy]["wins"] += 1 results = [] for strategy, data in metrics.items(): total = int(data["total"]) evaluated = int(data["evaluated"]) win_rate = round((data["wins"] / evaluated) * 100, 1) if evaluated > 0 else None avg_return = round(data["sum_return"] / evaluated, 2) if evaluated > 0 else None results.append( { "Strategy": strategy, "Win Rate": win_rate, "Avg Return": avg_return, "Count": total, } ) return results stats = load_statistics() by_strategy = stats.get("by_strategy", {}) if stats else {} results = [] for strategy, data in by_strategy.items(): win_rate = data.get("win_rate_7d") or data.get("win_rate", 0) avg_return = data.get("avg_return_7d", 0) count = data.get("count", data.get("wins_7d", 0) + data.get("losses_7d", 0)) results.append( { "Strategy": normalize_strategy(strategy), "Win Rate": win_rate, "Avg Return": avg_return, "Count": count, } ) return results def load_quick_stats() -> Tuple[int, float]: """ Load quick statistics for the sidebar. Returns: Tuple of (open_positions_count, win_rate_percentage) """ # Load open positions positions = load_open_positions() open_positions_count = len(positions) # Calculate win rate from statistics stats = load_statistics() win_rate = 0.0 if stats and "trades" in stats and len(stats["trades"]) > 0: winning_trades = sum( 1 for trade in stats["trades"] if trade.get("status") == "closed" and trade.get("profit", 0) > 0 ) total_trades = sum(1 for trade in stats["trades"] if trade.get("status") == "closed") if total_trades > 0: win_rate = (winning_trades / total_trades) * 100 return open_positions_count, win_rate