314 lines
9.6 KiB
Python
314 lines
9.6 KiB
Python
"""
|
|
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
|