TradingAgents/scripts/track_recommendation_perfor...

314 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Daily Performance Tracker
Tracks the performance of historical recommendations and updates the database.
Run this daily (via cron or manually) to monitor how recommendations perform over time.
Usage:
python scripts/track_recommendation_performance.py
Cron example (runs daily at 5pm after market close):
0 17 * * 1-5 cd /path/to/TradingAgents && python scripts/track_recommendation_performance.py
"""
import glob
import json
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List
# Add parent directory to path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from tradingagents.dataflows.y_finance import get_stock_price
from tradingagents.utils.logger import get_logger
logger = get_logger(__name__)
def load_recommendations() -> List[Dict[str, Any]]:
"""Load all historical recommendations from the recommendations directory."""
recommendations_dir = "data/recommendations"
if not os.path.exists(recommendations_dir):
logger.warning(f"No recommendations directory found at {recommendations_dir}")
return []
all_recs = []
pattern = os.path.join(recommendations_dir, "*.json")
for filepath in glob.glob(pattern):
try:
with open(filepath, "r") as f:
data = json.load(f)
# Each file contains recommendations from one discovery run
recs = data.get("recommendations", [])
run_date = data.get("date", os.path.basename(filepath).replace(".json", ""))
for rec in recs:
rec["discovery_date"] = run_date
all_recs.append(rec)
except Exception as e:
logger.error(f"Error loading {filepath}: {e}")
return all_recs
def update_performance(recommendations: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Update performance metrics for all recommendations."""
today = datetime.now().strftime("%Y-%m-%d")
for rec in recommendations:
ticker = rec.get("ticker")
discovery_date = rec.get("discovery_date")
entry_price = rec.get("entry_price")
if not all([ticker, discovery_date, entry_price]):
continue
# Skip if already marked as closed
if rec.get("status") == "closed":
continue
try:
# Get current price
current_price_data = get_stock_price(ticker, curr_date=today)
# Parse the price from the response (it returns a markdown report)
# Format is typically: "**Current Price**: $XXX.XX"
import re
price_match = re.search(r"\$([0-9,.]+)", current_price_data)
if price_match:
current_price = float(price_match.group(1).replace(",", ""))
else:
logger.warning(f"Could not parse price for {ticker}")
continue
# Calculate days since recommendation
rec_date = datetime.strptime(discovery_date, "%Y-%m-%d")
days_held = (datetime.now() - rec_date).days
# Calculate return
return_pct = ((current_price - entry_price) / entry_price) * 100
# Update metrics
rec["current_price"] = current_price
rec["return_pct"] = round(return_pct, 2)
rec["days_held"] = days_held
rec["last_updated"] = today
# Check specific time periods
if days_held >= 7 and "return_7d" not in rec:
rec["return_7d"] = round(return_pct, 2)
if days_held >= 30 and "return_30d" not in rec:
rec["return_30d"] = round(return_pct, 2)
rec["status"] = "closed" # Mark as complete after 30 days
# Determine win/loss for completed periods
if "return_7d" in rec:
rec["win_7d"] = rec["return_7d"] > 0
if "return_30d" in rec:
rec["win_30d"] = rec["return_30d"] > 0
logger.info(
f"{ticker}: Entry ${entry_price:.2f} → Current ${current_price:.2f} ({return_pct:+.1f}%) [{days_held}d]"
)
except Exception as e:
logger.error(f"✗ Error tracking {ticker}: {e}")
return recommendations
def save_performance_database(recommendations: List[Dict[str, Any]]):
"""Save the updated performance database."""
db_path = "data/recommendations/performance_database.json"
# Group by discovery date for organized storage
by_date = {}
for rec in recommendations:
date = rec.get("discovery_date", "unknown")
if date not in by_date:
by_date[date] = []
by_date[date].append(rec)
database = {
"last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"total_recommendations": len(recommendations),
"recommendations_by_date": by_date,
}
with open(db_path, "w") as f:
json.dump(database, f, indent=2)
logger.info(f"\n💾 Saved performance database to {db_path}")
def calculate_statistics(recommendations: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Calculate aggregate statistics from historical performance."""
stats = {
"total_recommendations": len(recommendations),
"by_strategy": {},
"overall_7d": {"count": 0, "wins": 0, "avg_return": 0},
"overall_30d": {"count": 0, "wins": 0, "avg_return": 0},
}
# Calculate by strategy
for rec in recommendations:
strategy = rec.get("strategy_match", "unknown")
if strategy not in stats["by_strategy"]:
stats["by_strategy"][strategy] = {
"count": 0,
"wins_7d": 0,
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_7d": 0,
"avg_return_30d": 0,
}
stats["by_strategy"][strategy]["count"] += 1
# 7-day stats
if "return_7d" in rec:
stats["overall_7d"]["count"] += 1
if rec.get("win_7d"):
stats["overall_7d"]["wins"] += 1
stats["by_strategy"][strategy]["wins_7d"] += 1
else:
stats["by_strategy"][strategy]["losses_7d"] += 1
stats["overall_7d"]["avg_return"] += rec["return_7d"]
# 30-day stats
if "return_30d" in rec:
stats["overall_30d"]["count"] += 1
if rec.get("win_30d"):
stats["overall_30d"]["wins"] += 1
stats["by_strategy"][strategy]["wins_30d"] += 1
else:
stats["by_strategy"][strategy]["losses_30d"] += 1
stats["overall_30d"]["avg_return"] += rec["return_30d"]
# Calculate averages and win rates
if stats["overall_7d"]["count"] > 0:
stats["overall_7d"]["win_rate"] = round(
(stats["overall_7d"]["wins"] / stats["overall_7d"]["count"]) * 100, 1
)
stats["overall_7d"]["avg_return"] = round(
stats["overall_7d"]["avg_return"] / stats["overall_7d"]["count"], 2
)
if stats["overall_30d"]["count"] > 0:
stats["overall_30d"]["win_rate"] = round(
(stats["overall_30d"]["wins"] / stats["overall_30d"]["count"]) * 100, 1
)
stats["overall_30d"]["avg_return"] = round(
stats["overall_30d"]["avg_return"] / stats["overall_30d"]["count"], 2
)
# Calculate per-strategy stats
for strategy, data in stats["by_strategy"].items():
total_7d = data["wins_7d"] + data["losses_7d"]
total_30d = data["wins_30d"] + data["losses_30d"]
if total_7d > 0:
data["win_rate_7d"] = round((data["wins_7d"] / total_7d) * 100, 1)
if total_30d > 0:
data["win_rate_30d"] = round((data["wins_30d"] / total_30d) * 100, 1)
return stats
def print_statistics(stats: Dict[str, Any]):
"""Print formatted statistics report."""
logger.info("\n" + "=" * 60)
logger.info("RECOMMENDATION PERFORMANCE STATISTICS")
logger.info("=" * 60)
logger.info(f"\nTotal Recommendations Tracked: {stats['total_recommendations']}")
# Overall stats
logger.info("\n📊 OVERALL PERFORMANCE")
logger.info("-" * 60)
if stats["overall_7d"]["count"] > 0:
logger.info("7-Day Performance:")
logger.info(f" • Tracked: {stats['overall_7d']['count']} recommendations")
logger.info(f" • Win Rate: {stats['overall_7d']['win_rate']}%")
logger.info(f" • Avg Return: {stats['overall_7d']['avg_return']:+.2f}%")
if stats["overall_30d"]["count"] > 0:
logger.info("\n30-Day Performance:")
logger.info(f" • Tracked: {stats['overall_30d']['count']} recommendations")
logger.info(f" • Win Rate: {stats['overall_30d']['win_rate']}%")
logger.info(f" • Avg Return: {stats['overall_30d']['avg_return']:+.2f}%")
# By strategy
if stats["by_strategy"]:
logger.info("\n📈 PERFORMANCE BY STRATEGY")
logger.info("-" * 60)
# Sort by win rate (if available)
sorted_strategies = sorted(
stats["by_strategy"].items(), key=lambda x: x[1].get("win_rate_7d", 0), reverse=True
)
for strategy, data in sorted_strategies:
logger.info(f"\n{strategy}:")
logger.info(f" • Total: {data['count']} recommendations")
if data.get("win_rate_7d"):
logger.info(
f" • 7-Day Win Rate: {data['win_rate_7d']}% ({data['wins_7d']}W/{data['losses_7d']}L)"
)
if data.get("win_rate_30d"):
logger.info(
f" • 30-Day Win Rate: {data['win_rate_30d']}% ({data['wins_30d']}W/{data['losses_30d']}L)"
)
def main():
"""Main execution function."""
logger.info("🔍 Loading historical recommendations...")
recommendations = load_recommendations()
if not recommendations:
logger.warning("No recommendations found to track.")
return
logger.info(f"Found {len(recommendations)} total recommendations")
# Filter to only track open positions (not closed after 30 days)
open_recs = [r for r in recommendations if r.get("status") != "closed"]
logger.info(f"Tracking {len(open_recs)} open positions...")
logger.info("\n📊 Updating performance metrics...\n")
updated_recs = update_performance(recommendations)
logger.info("\n📈 Calculating statistics...")
stats = calculate_statistics(updated_recs)
print_statistics(stats)
save_performance_database(updated_recs)
# Also save stats separately
stats_path = "data/recommendations/statistics.json"
with open(stats_path, "w") as f:
json.dump(stats, f, indent=2)
logger.info(f"💾 Saved statistics to {stats_path}")
logger.info("\n✅ Performance tracking complete!")
if __name__ == "__main__":
main()