441 lines
17 KiB
Python
441 lines
17 KiB
Python
"""
|
|
Historical Memory Builder for TradingAgents
|
|
|
|
This module creates agent memories from historical stock data by:
|
|
1. Analyzing market conditions at time T
|
|
2. Observing actual stock performance at time T + delta
|
|
3. Creating situation -> outcome mappings for each agent type
|
|
4. Storing memories in ChromaDB for future retrieval
|
|
"""
|
|
|
|
import os
|
|
from datetime import datetime, timedelta
|
|
from typing import List, Dict, Tuple, Optional
|
|
from tradingagents.tools.executor import execute_tool
|
|
from tradingagents.agents.utils.memory import FinancialSituationMemory
|
|
|
|
|
|
class HistoricalMemoryBuilder:
|
|
"""Build agent memories from historical stock data."""
|
|
|
|
def __init__(self, config: dict):
|
|
"""Initialize the memory builder.
|
|
|
|
Args:
|
|
config: TradingAgents configuration dictionary
|
|
"""
|
|
self.config = config
|
|
self.memories_created = {
|
|
"bull": 0,
|
|
"bear": 0,
|
|
"trader": 0,
|
|
"invest_judge": 0,
|
|
"risk_manager": 0
|
|
}
|
|
|
|
def _get_stock_data_for_period(self, ticker: str, date: str) -> Dict[str, str]:
|
|
"""Gather all available data for a stock on a specific date.
|
|
|
|
Args:
|
|
ticker: Stock ticker symbol
|
|
date: Date in YYYY-MM-DD format
|
|
|
|
Returns:
|
|
Dictionary with market_report, news_report, sentiment_report, fundamentals_report
|
|
"""
|
|
data = {}
|
|
|
|
try:
|
|
# Get technical/price data (what Market Analyst sees)
|
|
stock_data = execute_tool("get_stock_data", symbol=ticker, start_date=date)
|
|
indicators = execute_tool("get_indicators", symbol=ticker, start_date=date)
|
|
data["market_report"] = f"Stock Data:\n{stock_data}\n\nTechnical Indicators:\n{indicators}"
|
|
except Exception as e:
|
|
data["market_report"] = f"Error fetching market data: {e}"
|
|
|
|
try:
|
|
# Get news (what News Analyst sees)
|
|
news = execute_tool("get_news", symbol=ticker, from_date=date, to_date=date)
|
|
data["news_report"] = news
|
|
except Exception as e:
|
|
data["news_report"] = f"Error fetching news: {e}"
|
|
|
|
try:
|
|
# Get sentiment (what Social Analyst sees)
|
|
sentiment = execute_tool("get_reddit_discussions", symbol=ticker, from_date=date, to_date=date)
|
|
data["sentiment_report"] = sentiment
|
|
except Exception as e:
|
|
data["sentiment_report"] = f"Error fetching sentiment: {e}"
|
|
|
|
try:
|
|
# Get fundamentals (what Fundamentals Analyst sees)
|
|
fundamentals = execute_tool("get_fundamentals", symbol=ticker)
|
|
data["fundamentals_report"] = fundamentals
|
|
except Exception as e:
|
|
data["fundamentals_report"] = f"Error fetching fundamentals: {e}"
|
|
|
|
return data
|
|
|
|
def _calculate_returns(self, ticker: str, start_date: str, end_date: str) -> Optional[float]:
|
|
"""Calculate stock returns between two dates.
|
|
|
|
Args:
|
|
ticker: Stock ticker symbol
|
|
start_date: Starting date (YYYY-MM-DD)
|
|
end_date: Ending date (YYYY-MM-DD)
|
|
|
|
Returns:
|
|
Percentage return, or None if data unavailable
|
|
"""
|
|
try:
|
|
# Get stock prices for both dates
|
|
start_data = execute_tool("get_stock_data", symbol=ticker, start_date=start_date, end_date=start_date)
|
|
end_data = execute_tool("get_stock_data", symbol=ticker, start_date=end_date, end_date=end_date)
|
|
|
|
# Parse prices (this is simplified - you'd need to parse the actual response)
|
|
# Assuming response has close price - adjust based on actual API response
|
|
import re
|
|
start_match = re.search(r'Close[:\s]+\$?([\d.]+)', str(start_data))
|
|
end_match = re.search(r'Close[:\s]+\$?([\d.]+)', str(end_data))
|
|
|
|
if start_match and end_match:
|
|
start_price = float(start_match.group(1))
|
|
end_price = float(end_match.group(1))
|
|
return ((end_price - start_price) / start_price) * 100
|
|
|
|
return None
|
|
except Exception as e:
|
|
print(f"Error calculating returns: {e}")
|
|
return None
|
|
|
|
def _create_bull_researcher_memory(self, situation: str, returns: float, ticker: str, date: str) -> str:
|
|
"""Create memory for bull researcher based on outcome.
|
|
|
|
Returns lesson learned from bullish perspective.
|
|
"""
|
|
if returns > 5:
|
|
return f"""SUCCESSFUL BULLISH ANALYSIS for {ticker} on {date}:
|
|
The market conditions indicated strong bullish signals, and the stock delivered {returns:.2f}% returns.
|
|
|
|
Key takeaways:
|
|
- When similar conditions appear (strong fundamentals + positive sentiment + bullish technicals), aggressive BUY positions are warranted
|
|
- The combination of factors in this situation was a reliable indicator of upward momentum
|
|
- Continue to weight these signals heavily in future bullish arguments
|
|
|
|
Recommendation: In similar situations, advocate strongly for BUY positions with high conviction.
|
|
"""
|
|
elif returns < -5:
|
|
return f"""INCORRECT BULLISH SIGNALS for {ticker} on {date}:
|
|
Despite apparent bullish indicators, the stock declined {abs(returns):.2f}%.
|
|
|
|
Lessons learned:
|
|
- The bullish signals in this situation were misleading or outweighed by hidden risks
|
|
- Need to look deeper at: macro conditions, sector headwinds, or fundamental weaknesses that weren't apparent
|
|
- Be more cautious when similar patterns appear; consider bear arguments more seriously
|
|
|
|
Recommendation: In similar situations, temper bullish enthusiasm and scrutinize fundamentals more carefully.
|
|
"""
|
|
else:
|
|
return f"""NEUTRAL OUTCOME for {ticker} on {date}:
|
|
Stock moved {returns:.2f}%, indicating mixed signals.
|
|
|
|
Lesson: This pattern of indicators doesn't provide strong directional conviction. Look for clearer signals before making strong bullish arguments.
|
|
"""
|
|
|
|
def _create_bear_researcher_memory(self, situation: str, returns: float, ticker: str, date: str) -> str:
|
|
"""Create memory for bear researcher based on outcome."""
|
|
if returns < -5:
|
|
return f"""SUCCESSFUL BEARISH ANALYSIS for {ticker} on {date}:
|
|
Bearish indicators correctly predicted decline of {abs(returns):.2f}%.
|
|
|
|
Key takeaways:
|
|
- The risk factors identified were valid and material
|
|
- Similar warning signs should be treated seriously in future analysis
|
|
- When these patterns appear, advocate strongly for SELL or reduce positions
|
|
|
|
Recommendation: In similar situations, maintain bearish stance with high conviction.
|
|
"""
|
|
elif returns > 5:
|
|
return f"""INCORRECT BEARISH SIGNALS for {ticker} on {date}:
|
|
Despite bearish indicators, stock rallied {returns:.2f}%.
|
|
|
|
Lessons learned:
|
|
- The bearish concerns were either overstated or offset by stronger positive factors
|
|
- Market sentiment or momentum can override fundamental concerns in short term
|
|
- Need to better assess whether bearish factors are already priced in
|
|
|
|
Recommendation: In similar situations, be more cautious about strong SELL recommendations.
|
|
"""
|
|
else:
|
|
return f"""NEUTRAL OUTCOME for {ticker} on {date}:
|
|
Stock moved {returns:.2f}%, mixed signals.
|
|
|
|
Lesson: These indicators don't provide clear bearish conviction. Need stronger warning signs for definitive bearish stance.
|
|
"""
|
|
|
|
def _create_trader_memory(self, situation: str, returns: float, ticker: str, date: str) -> str:
|
|
"""Create memory for trader based on outcome."""
|
|
if abs(returns) < 2:
|
|
action = "HOLD"
|
|
result = "correct - low volatility"
|
|
elif returns > 5:
|
|
action = "BUY"
|
|
result = "would have been optimal"
|
|
elif returns < -5:
|
|
action = "SELL or avoid"
|
|
result = "would have been optimal"
|
|
else:
|
|
action = "modest position"
|
|
result = "moderate returns"
|
|
|
|
return f"""TRADING OUTCOME for {ticker} on {date}:
|
|
Stock returned {returns:.2f}% over the evaluation period.
|
|
|
|
Optimal action: {action} - {result}
|
|
|
|
Market conditions at the time:
|
|
{situation[:500]}...
|
|
|
|
Trading lesson:
|
|
- When similar market conditions appear, consider {action} strategy
|
|
- Risk/reward profile: {'Favorable' if abs(returns) > 3 else 'Neutral'}
|
|
- Position sizing: {'Aggressive' if abs(returns) > 7 else 'Moderate' if abs(returns) > 3 else 'Conservative'}
|
|
|
|
Recommendation: Pattern recognition suggests {action} in similar future scenarios.
|
|
"""
|
|
|
|
def _create_invest_judge_memory(self, situation: str, returns: float, ticker: str, date: str) -> str:
|
|
"""Create memory for investment judge/research manager."""
|
|
if returns > 5:
|
|
verdict = "Strong BUY recommendation was warranted"
|
|
elif returns > 2:
|
|
verdict = "Moderate BUY recommendation was appropriate"
|
|
elif returns < -5:
|
|
verdict = "SELL or AVOID recommendation was warranted"
|
|
elif returns < -2:
|
|
verdict = "HOLD or reduce exposure was appropriate"
|
|
else:
|
|
verdict = "HOLD recommendation was appropriate"
|
|
|
|
return f"""INVESTMENT DECISION REVIEW for {ticker} on {date}:
|
|
Actual outcome: {returns:.2f}% return
|
|
|
|
Optimal decision: {verdict}
|
|
|
|
When synthesizing bull/bear arguments in similar conditions:
|
|
- Weight the arguments based on which perspective proved more accurate
|
|
- {"Bull arguments were stronger" if returns > 0 else "Bear arguments were stronger"}
|
|
- Factor reliability: {'High' if abs(returns) > 5 else 'Medium' if abs(returns) > 2 else 'Low'}
|
|
|
|
Recommendation for similar situations: {verdict}
|
|
"""
|
|
|
|
def _create_risk_manager_memory(self, situation: str, returns: float, ticker: str, date: str) -> str:
|
|
"""Create memory for risk manager."""
|
|
volatility = "HIGH" if abs(returns) > 10 else "MEDIUM" if abs(returns) > 5 else "LOW"
|
|
|
|
if abs(returns) > 10:
|
|
risk_assessment = "High risk - extreme volatility observed"
|
|
elif abs(returns) > 5:
|
|
risk_assessment = "Moderate risk - significant movement"
|
|
else:
|
|
risk_assessment = "Low risk - stable price action"
|
|
|
|
return f"""RISK ASSESSMENT REVIEW for {ticker} on {date}:
|
|
Observed volatility: {volatility} (actual return: {returns:.2f}%)
|
|
|
|
Risk factors that materialized:
|
|
- Price volatility: {volatility}
|
|
- Directional risk: {'Significant downside' if returns < -5 else 'Significant upside' if returns > 5 else 'Minimal'}
|
|
|
|
Risk management lesson:
|
|
In similar market conditions:
|
|
- Position size: {'Small (high risk)' if abs(returns) > 10 else 'Moderate' if abs(returns) > 5 else 'Standard'}
|
|
- Stop loss: {'Tight (±5%)' if abs(returns) > 10 else 'Standard (±7%)' if abs(returns) > 5 else 'Relaxed (±10%)'}
|
|
- Diversification: {'Critical' if abs(returns) > 10 else 'Recommended' if abs(returns) > 5 else 'Standard'}
|
|
|
|
Recommendation: {risk_assessment}
|
|
"""
|
|
|
|
def build_memories_for_stock(
|
|
self,
|
|
ticker: str,
|
|
start_date: str,
|
|
end_date: str,
|
|
lookforward_days: int = 7,
|
|
interval_days: int = 30
|
|
) -> Dict[str, List[Tuple[str, str]]]:
|
|
"""Build historical memories for a stock across a date range.
|
|
|
|
Args:
|
|
ticker: Stock ticker symbol
|
|
start_date: Start date (YYYY-MM-DD)
|
|
end_date: End date (YYYY-MM-DD)
|
|
lookforward_days: How many days forward to measure returns (default: 7)
|
|
interval_days: Days between memory samples (default: 30)
|
|
|
|
Returns:
|
|
Dictionary mapping agent type to list of (situation, lesson) tuples
|
|
"""
|
|
memories = {
|
|
"bull": [],
|
|
"bear": [],
|
|
"trader": [],
|
|
"invest_judge": [],
|
|
"risk_manager": []
|
|
}
|
|
|
|
current_date = datetime.strptime(start_date, "%Y-%m-%d")
|
|
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
|
|
|
|
print(f"\n🧠 Building historical memories for {ticker}")
|
|
print(f" Period: {start_date} to {end_date}")
|
|
print(f" Lookforward: {lookforward_days} days")
|
|
print(f" Sampling interval: {interval_days} days\n")
|
|
|
|
sample_count = 0
|
|
while current_date <= end_dt:
|
|
date_str = current_date.strftime("%Y-%m-%d")
|
|
future_date_str = (current_date + timedelta(days=lookforward_days)).strftime("%Y-%m-%d")
|
|
|
|
print(f" 📊 Sampling {date_str}...", end=" ")
|
|
|
|
# Get historical data for this period
|
|
data = self._get_stock_data_for_period(ticker, date_str)
|
|
situation = f"{data['market_report']}\n\n{data['sentiment_report']}\n\n{data['news_report']}\n\n{data['fundamentals_report']}"
|
|
|
|
# Calculate actual returns
|
|
returns = self._calculate_returns(ticker, date_str, future_date_str)
|
|
|
|
if returns is not None:
|
|
print(f"Return: {returns:+.2f}%")
|
|
|
|
# Create agent-specific memories
|
|
memories["bull"].append((
|
|
situation,
|
|
self._create_bull_researcher_memory(situation, returns, ticker, date_str)
|
|
))
|
|
|
|
memories["bear"].append((
|
|
situation,
|
|
self._create_bear_researcher_memory(situation, returns, ticker, date_str)
|
|
))
|
|
|
|
memories["trader"].append((
|
|
situation,
|
|
self._create_trader_memory(situation, returns, ticker, date_str)
|
|
))
|
|
|
|
memories["invest_judge"].append((
|
|
situation,
|
|
self._create_invest_judge_memory(situation, returns, ticker, date_str)
|
|
))
|
|
|
|
memories["risk_manager"].append((
|
|
situation,
|
|
self._create_risk_manager_memory(situation, returns, ticker, date_str)
|
|
))
|
|
|
|
sample_count += 1
|
|
else:
|
|
print("⚠️ No data")
|
|
|
|
# Move to next interval
|
|
current_date += timedelta(days=interval_days)
|
|
|
|
print(f"\n✅ Created {sample_count} memory samples for {ticker}")
|
|
for agent_type in memories:
|
|
self.memories_created[agent_type] += len(memories[agent_type])
|
|
|
|
return memories
|
|
|
|
def populate_agent_memories(
|
|
self,
|
|
tickers: List[str],
|
|
start_date: str,
|
|
end_date: str,
|
|
lookforward_days: int = 7,
|
|
interval_days: int = 30
|
|
) -> Dict[str, FinancialSituationMemory]:
|
|
"""Build and populate memories for all agent types across multiple stocks.
|
|
|
|
Args:
|
|
tickers: List of stock ticker symbols
|
|
start_date: Start date for historical analysis
|
|
end_date: End date for historical analysis
|
|
lookforward_days: Days forward to measure returns
|
|
interval_days: Days between samples
|
|
|
|
Returns:
|
|
Dictionary of populated memory instances for each agent type
|
|
"""
|
|
# Initialize memory stores
|
|
agent_memories = {
|
|
"bull": FinancialSituationMemory("bull_memory", self.config),
|
|
"bear": FinancialSituationMemory("bear_memory", self.config),
|
|
"trader": FinancialSituationMemory("trader_memory", self.config),
|
|
"invest_judge": FinancialSituationMemory("invest_judge_memory", self.config),
|
|
"risk_manager": FinancialSituationMemory("risk_manager_memory", self.config)
|
|
}
|
|
|
|
print("=" * 70)
|
|
print("🏗️ HISTORICAL MEMORY BUILDER")
|
|
print("=" * 70)
|
|
|
|
# Build memories for each ticker
|
|
for ticker in tickers:
|
|
memories = self.build_memories_for_stock(
|
|
ticker=ticker,
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
lookforward_days=lookforward_days,
|
|
interval_days=interval_days
|
|
)
|
|
|
|
# Add memories to each agent's memory store
|
|
for agent_type, memory_list in memories.items():
|
|
if memory_list:
|
|
agent_memories[agent_type].add_situations(memory_list)
|
|
|
|
# Print summary
|
|
print("\n" + "=" * 70)
|
|
print("📊 MEMORY CREATION SUMMARY")
|
|
print("=" * 70)
|
|
for agent_type, count in self.memories_created.items():
|
|
print(f" {agent_type.ljust(15)}: {count} memories")
|
|
print("=" * 70 + "\n")
|
|
|
|
return agent_memories
|
|
|
|
|
|
# Example usage
|
|
if __name__ == "__main__":
|
|
from tradingagents.default_config import DEFAULT_CONFIG
|
|
|
|
# Initialize builder
|
|
builder = HistoricalMemoryBuilder(DEFAULT_CONFIG)
|
|
|
|
# Build memories for specific stocks over past year
|
|
tickers = ["AAPL", "GOOGL", "MSFT", "NVDA", "TSLA"]
|
|
|
|
memories = builder.populate_agent_memories(
|
|
tickers=tickers,
|
|
start_date="2024-01-01",
|
|
end_date="2024-12-01",
|
|
lookforward_days=7, # 1-week returns
|
|
interval_days=30 # Sample monthly
|
|
)
|
|
|
|
# Test retrieval
|
|
test_situation = "Strong earnings beat with positive sentiment and bullish technical indicators in tech sector"
|
|
|
|
print("\n🔍 Testing memory retrieval...")
|
|
print(f"Query: {test_situation}\n")
|
|
|
|
for agent_type, memory in memories.items():
|
|
print(f"\n{agent_type.upper()} MEMORIES:")
|
|
results = memory.get_memories(test_situation, n_matches=2)
|
|
for i, result in enumerate(results, 1):
|
|
print(f"\n Match {i} (similarity: {result['similarity_score']:.2f}):")
|
|
print(f" {result['recommendation'][:200]}...")
|