feat: add Claude Code subagent pipeline (no API keys required)
Adds cc_tools.py CLI wrapper for data gathering and a trading-analysis skill that orchestrates ~12 specialized subagents through Claude Code's Agent tool, replacing the LangGraph pipeline's dependency on external LLM API keys. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
589b351f2a
commit
72f34187c1
|
|
@ -0,0 +1,312 @@
|
||||||
|
---
|
||||||
|
name: trading-analysis
|
||||||
|
description: Run the TradingAgents multi-agent trading analysis pipeline for a stock ticker. Launches specialized analyst, researcher, debater, and portfolio manager subagents to produce a final BUY/OVERWEIGHT/HOLD/UNDERWEIGHT/SELL recommendation.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Trading Analysis Skill
|
||||||
|
|
||||||
|
Analyze a stock using the TradingAgents multi-agent framework. This skill orchestrates ~12 specialized agents through Claude Code's Agent tool, replicating the original LangGraph pipeline.
|
||||||
|
|
||||||
|
## Input
|
||||||
|
|
||||||
|
The user provides:
|
||||||
|
- **TICKER**: Stock ticker symbol (e.g., NVDA, AAPL, MSFT, CNC.TO)
|
||||||
|
- **TRADE_DATE**: Analysis date in yyyy-mm-dd format (defaults to today if not specified)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- `max_debate_rounds`: 1 (bull/bear debate rounds)
|
||||||
|
- `max_risk_discuss_rounds`: 1 (risk analyst discussion rounds)
|
||||||
|
- Python executable: Use the project's venv at `.venv/Scripts/python.exe` (Windows) or `.venv/bin/python` (Unix)
|
||||||
|
- All data tool calls go through: `python cc_tools.py <command> [args]` from the project root
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before starting, verify the environment:
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python.exe cc_tools.py --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Execution Flow
|
||||||
|
|
||||||
|
### PHASE 1: Analyst Reports (PARALLEL - launch all 4 simultaneously)
|
||||||
|
|
||||||
|
Launch exactly 4 Agent subagents **in a single message** (parallel). Each subagent should use `model: "sonnet"` for cost efficiency. Each one collects data via Bash and returns its report.
|
||||||
|
|
||||||
|
**Important:** Tell each subagent to run commands from the project root directory. The python executable path is `.venv/Scripts/python.exe` on Windows.
|
||||||
|
|
||||||
|
#### 1a. Market Analyst Subagent
|
||||||
|
|
||||||
|
Prompt for the Agent tool:
|
||||||
|
|
||||||
|
> You are a Market Analyst. Your job is to analyze technical indicators for {TICKER} as of {TRADE_DATE}.
|
||||||
|
>
|
||||||
|
> Use Bash to call these commands from the project root to gather data:
|
||||||
|
> - `.venv/Scripts/python.exe cc_tools.py get_stock_data {TICKER} {START_DATE} {TRADE_DATE}` (START_DATE = 30 days before TRADE_DATE)
|
||||||
|
> - `.venv/Scripts/python.exe cc_tools.py get_indicators {TICKER} <indicator_name> {TRADE_DATE}` for each indicator
|
||||||
|
>
|
||||||
|
> Select up to 8 of the most relevant technical indicators from: close_50_sma, close_200_sma, close_10_ema, macd, macds, macdh, rsi, boll, boll_ub, boll_lb, atr, vwma. Avoid redundancy. Call get_stock_data first, then get_indicators for each selected indicator.
|
||||||
|
>
|
||||||
|
> Write a detailed, nuanced report of the trends you observe. Provide specific, actionable insights with supporting evidence. Append a Markdown table at the end organizing key points.
|
||||||
|
>
|
||||||
|
> The instrument to analyze is `{TICKER}`. Use this exact ticker in every tool call and report.
|
||||||
|
|
||||||
|
#### 1b. Social Media Analyst Subagent
|
||||||
|
|
||||||
|
> You are a Social Media and Sentiment Analyst. Analyze social media posts, company news, and public sentiment for {TICKER} over the past week as of {TRADE_DATE}.
|
||||||
|
>
|
||||||
|
> Use Bash to call: `.venv/Scripts/python.exe cc_tools.py get_news {TICKER} {START_DATE} {TRADE_DATE}` (START_DATE = 7 days before TRADE_DATE)
|
||||||
|
>
|
||||||
|
> Write a comprehensive report analyzing sentiment, social media discussion, and recent company news. Provide specific, actionable insights. Append a Markdown table at the end.
|
||||||
|
>
|
||||||
|
> The instrument to analyze is `{TICKER}`.
|
||||||
|
|
||||||
|
#### 1c. News Analyst Subagent
|
||||||
|
|
||||||
|
> You are a News and Macroeconomic Analyst. Analyze recent news and trends relevant to trading {TICKER} as of {TRADE_DATE}.
|
||||||
|
>
|
||||||
|
> Use Bash to call:
|
||||||
|
> - `.venv/Scripts/python.exe cc_tools.py get_news {TICKER} {START_DATE} {TRADE_DATE}` (START_DATE = 7 days before)
|
||||||
|
> - `.venv/Scripts/python.exe cc_tools.py get_global_news {TRADE_DATE} 7 5`
|
||||||
|
>
|
||||||
|
> Write a comprehensive report of the global state relevant for trading and macroeconomics. Provide specific, actionable insights. Append a Markdown table at the end.
|
||||||
|
>
|
||||||
|
> The instrument to analyze is `{TICKER}`.
|
||||||
|
|
||||||
|
#### 1d. Fundamentals Analyst Subagent
|
||||||
|
|
||||||
|
> You are a Fundamentals Analyst. Analyze the fundamental financial information for {TICKER} as of {TRADE_DATE}.
|
||||||
|
>
|
||||||
|
> Use Bash to call:
|
||||||
|
> - `.venv/Scripts/python.exe cc_tools.py get_fundamentals {TICKER} {TRADE_DATE}`
|
||||||
|
> - `.venv/Scripts/python.exe cc_tools.py get_balance_sheet {TICKER} quarterly {TRADE_DATE}`
|
||||||
|
> - `.venv/Scripts/python.exe cc_tools.py get_cashflow {TICKER} quarterly {TRADE_DATE}`
|
||||||
|
> - `.venv/Scripts/python.exe cc_tools.py get_income_statement {TICKER} quarterly {TRADE_DATE}`
|
||||||
|
>
|
||||||
|
> Write a comprehensive report of company fundamentals including financial documents, company profile, financials, and history. Provide specific, actionable insights with a Markdown table summary.
|
||||||
|
>
|
||||||
|
> The instrument to analyze is `{TICKER}`.
|
||||||
|
|
||||||
|
After all 4 return, save their outputs as:
|
||||||
|
- `market_report` = Market Analyst result
|
||||||
|
- `sentiment_report` = Social Media Analyst result
|
||||||
|
- `news_report` = News Analyst result
|
||||||
|
- `fundamentals_report` = Fundamentals Analyst result
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PHASE 2: Investment Debate (SEQUENTIAL)
|
||||||
|
|
||||||
|
Run 1 round of bull/bear debate (configurable via max_debate_rounds).
|
||||||
|
|
||||||
|
#### 2a. Bull Researcher Subagent
|
||||||
|
|
||||||
|
Launch an Agent with this prompt:
|
||||||
|
|
||||||
|
> You are a Bull Analyst advocating for investing in {TICKER}. Build a strong, evidence-based case emphasizing growth potential, competitive advantages, and positive market indicators.
|
||||||
|
>
|
||||||
|
> Key points to focus on:
|
||||||
|
> - Growth Potential: Highlight market opportunities, revenue projections, and scalability
|
||||||
|
> - Competitive Advantages: Emphasize unique products, strong branding, or dominant market positioning
|
||||||
|
> - Positive Indicators: Use financial health, industry trends, and positive news as evidence
|
||||||
|
> - Engagement: Present conversationally, engaging directly as if debating
|
||||||
|
>
|
||||||
|
> Resources:
|
||||||
|
> Market report: {market_report}
|
||||||
|
> Sentiment report: {sentiment_report}
|
||||||
|
> News report: {news_report}
|
||||||
|
> Fundamentals report: {fundamentals_report}
|
||||||
|
> Last bear argument: (none yet - this is the opening round)
|
||||||
|
>
|
||||||
|
> Deliver a compelling bull argument. Do NOT use any tools - just analyze the provided data and argue your case.
|
||||||
|
|
||||||
|
Save result as `bull_argument`.
|
||||||
|
|
||||||
|
#### 2b. Bear Researcher Subagent
|
||||||
|
|
||||||
|
Launch an Agent with this prompt:
|
||||||
|
|
||||||
|
> You are a Bear Analyst advocating AGAINST investing in {TICKER}. Present a well-reasoned case emphasizing risks, challenges, and negative indicators.
|
||||||
|
>
|
||||||
|
> Key points to focus on:
|
||||||
|
> - Risks: Highlight market saturation, financial instability, macroeconomic threats
|
||||||
|
> - Competitive Weaknesses: Emphasize weaker market position, innovation decline
|
||||||
|
> - Negative Indicators: Use financial data, market trends, adverse news as evidence
|
||||||
|
> - Counter the bull argument with specific data and reasoning
|
||||||
|
> - Engagement: Present conversationally, as if debating
|
||||||
|
>
|
||||||
|
> Resources:
|
||||||
|
> Market report: {market_report}
|
||||||
|
> Sentiment report: {sentiment_report}
|
||||||
|
> News report: {news_report}
|
||||||
|
> Fundamentals report: {fundamentals_report}
|
||||||
|
> Bull argument to counter: {bull_argument}
|
||||||
|
>
|
||||||
|
> Deliver a compelling bear argument countering the bull case. Do NOT use any tools.
|
||||||
|
|
||||||
|
Save result as `bear_argument`.
|
||||||
|
|
||||||
|
Build the debate state:
|
||||||
|
- `debate_history` = "Bull Analyst: {bull_argument}\nBear Analyst: {bear_argument}"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PHASE 3: Research Manager (SEQUENTIAL)
|
||||||
|
|
||||||
|
Launch an Agent:
|
||||||
|
|
||||||
|
> As the Research Manager and debate facilitator, critically evaluate this debate and make a DEFINITIVE decision: align with the bull analyst, bear analyst, or Hold (only if strongly justified).
|
||||||
|
>
|
||||||
|
> Summarize key points from both sides concisely. Your recommendation -- Buy, Sell, or Hold -- must be clear and actionable, grounded in the debate's strongest arguments. Avoid defaulting to Hold simply because both sides have valid points.
|
||||||
|
>
|
||||||
|
> Develop a detailed investment plan including:
|
||||||
|
> 1. Your Recommendation: A decisive stance
|
||||||
|
> 2. Rationale: Why these arguments lead to your conclusion
|
||||||
|
> 3. Strategic Actions: Concrete implementation steps
|
||||||
|
>
|
||||||
|
> The instrument is `{TICKER}`.
|
||||||
|
>
|
||||||
|
> Debate History:
|
||||||
|
> {debate_history}
|
||||||
|
>
|
||||||
|
> Do NOT use any tools.
|
||||||
|
|
||||||
|
Save result as `investment_plan`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PHASE 4: Trader (SEQUENTIAL)
|
||||||
|
|
||||||
|
Launch an Agent:
|
||||||
|
|
||||||
|
> You are a Trading Agent analyzing market data to make investment decisions for {TICKER}.
|
||||||
|
>
|
||||||
|
> Based on a comprehensive analysis by a team of analysts, here is an investment plan. Use it as a foundation for your trading decision.
|
||||||
|
>
|
||||||
|
> Proposed Investment Plan: {investment_plan}
|
||||||
|
>
|
||||||
|
> Additional context:
|
||||||
|
> Market report: {market_report}
|
||||||
|
> Sentiment report: {sentiment_report}
|
||||||
|
> News report: {news_report}
|
||||||
|
> Fundamentals report: {fundamentals_report}
|
||||||
|
>
|
||||||
|
> Provide a specific recommendation to buy, sell, or hold. End with a firm decision and conclude your response with "FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL**".
|
||||||
|
>
|
||||||
|
> The instrument is `{TICKER}`. Do NOT use any tools.
|
||||||
|
|
||||||
|
Save result as `trader_plan`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PHASE 5: Risk Debate (SEQUENTIAL - 3 analysts)
|
||||||
|
|
||||||
|
Run 1 round of aggressive/conservative/neutral debate.
|
||||||
|
|
||||||
|
#### 5a. Aggressive Risk Analyst Subagent
|
||||||
|
|
||||||
|
> As the Aggressive Risk Analyst, champion high-reward, high-risk opportunities for {TICKER}. Evaluate the trader's decision focusing on potential upside, growth potential, and innovative benefits.
|
||||||
|
>
|
||||||
|
> Trader's decision: {trader_plan}
|
||||||
|
>
|
||||||
|
> Market Research Report: {market_report}
|
||||||
|
> Sentiment Report: {sentiment_report}
|
||||||
|
> News Report: {news_report}
|
||||||
|
> Fundamentals Report: {fundamentals_report}
|
||||||
|
>
|
||||||
|
> Present your argument based on the data. Focus on debating and persuading, not just presenting data. Output conversationally without special formatting. Do NOT use any tools.
|
||||||
|
|
||||||
|
Save result as `aggressive_argument`.
|
||||||
|
|
||||||
|
#### 5b. Conservative Risk Analyst Subagent
|
||||||
|
|
||||||
|
> As the Conservative Risk Analyst, protect assets, minimize volatility, and ensure steady growth for {TICKER}. Critically examine high-risk elements in the trader's decision.
|
||||||
|
>
|
||||||
|
> Trader's decision: {trader_plan}
|
||||||
|
>
|
||||||
|
> Market Research Report: {market_report}
|
||||||
|
> Sentiment Report: {sentiment_report}
|
||||||
|
> News Report: {news_report}
|
||||||
|
> Fundamentals Report: {fundamentals_report}
|
||||||
|
>
|
||||||
|
> Aggressive analyst's argument: {aggressive_argument}
|
||||||
|
>
|
||||||
|
> Counter the aggressive stance. Emphasize potential downsides they overlooked. Focus on debating and critiquing. Output conversationally without special formatting. Do NOT use any tools.
|
||||||
|
|
||||||
|
Save result as `conservative_argument`.
|
||||||
|
|
||||||
|
#### 5c. Neutral Risk Analyst Subagent
|
||||||
|
|
||||||
|
> As the Neutral Risk Analyst, provide a balanced perspective on {TICKER}, weighing both potential benefits and risks. Challenge both the aggressive and conservative views.
|
||||||
|
>
|
||||||
|
> Trader's decision: {trader_plan}
|
||||||
|
>
|
||||||
|
> Market Research Report: {market_report}
|
||||||
|
> Sentiment Report: {sentiment_report}
|
||||||
|
> News Report: {news_report}
|
||||||
|
> Fundamentals Report: {fundamentals_report}
|
||||||
|
>
|
||||||
|
> Aggressive analyst's argument: {aggressive_argument}
|
||||||
|
> Conservative analyst's argument: {conservative_argument}
|
||||||
|
>
|
||||||
|
> Analyze both sides critically. Advocate for a balanced approach offering growth with safeguards. Output conversationally without special formatting. Do NOT use any tools.
|
||||||
|
|
||||||
|
Save result as `neutral_argument`.
|
||||||
|
|
||||||
|
Build risk debate history:
|
||||||
|
- `risk_debate_history` = "Aggressive Analyst: {aggressive_argument}\nConservative Analyst: {conservative_argument}\nNeutral Analyst: {neutral_argument}"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PHASE 6: Portfolio Manager (SEQUENTIAL)
|
||||||
|
|
||||||
|
Launch an Agent:
|
||||||
|
|
||||||
|
> As the Portfolio Manager, synthesize the risk analysts' debate and deliver the FINAL trading decision for {TICKER}.
|
||||||
|
>
|
||||||
|
> **Rating Scale** (use exactly one):
|
||||||
|
> - **Buy**: Strong conviction to enter or add to position
|
||||||
|
> - **Overweight**: Favorable outlook, gradually increase exposure
|
||||||
|
> - **Hold**: Maintain current position, no action needed
|
||||||
|
> - **Underweight**: Reduce exposure, take partial profits
|
||||||
|
> - **Sell**: Exit position or avoid entry
|
||||||
|
>
|
||||||
|
> Trader's proposed plan: {trader_plan}
|
||||||
|
>
|
||||||
|
> **Required Output Structure:**
|
||||||
|
> 1. **Rating**: State one of Buy / Overweight / Hold / Underweight / Sell
|
||||||
|
> 2. **Executive Summary**: Concise action plan covering entry strategy, position sizing, key risk levels, and time horizon
|
||||||
|
> 3. **Investment Thesis**: Detailed reasoning anchored in the analysts' debate
|
||||||
|
>
|
||||||
|
> Risk Analysts Debate History:
|
||||||
|
> {risk_debate_history}
|
||||||
|
>
|
||||||
|
> Be decisive and ground every conclusion in specific evidence from the analysts. Do NOT use any tools.
|
||||||
|
|
||||||
|
Save result as `final_decision`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PHASE 7: Signal Extraction & Results
|
||||||
|
|
||||||
|
Extract the final rating from the portfolio manager's output. It should be exactly one of: **BUY**, **OVERWEIGHT**, **HOLD**, **UNDERWEIGHT**, or **SELL**.
|
||||||
|
|
||||||
|
Then save the full results by writing a JSON state file and calling:
|
||||||
|
```bash
|
||||||
|
.venv/Scripts/python.exe cc_tools.py save_results {TICKER} {TRADE_DATE} <state_json_file>
|
||||||
|
```
|
||||||
|
|
||||||
|
The state JSON should contain all fields: company_of_interest, trade_date, market_report, sentiment_report, news_report, fundamentals_report, investment_debate_state, investment_plan, trader_investment_plan, risk_debate_state, final_trade_decision.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PHASE 8: Present Results
|
||||||
|
|
||||||
|
Display a summary to the user:
|
||||||
|
|
||||||
|
1. **Final Rating**: The extracted BUY/OVERWEIGHT/HOLD/UNDERWEIGHT/SELL
|
||||||
|
2. **Executive Summary**: From the portfolio manager
|
||||||
|
3. **Key Reports**: Brief highlights from each analyst
|
||||||
|
4. **Debate Summary**: Key points from both investment and risk debates
|
||||||
|
|
||||||
|
The full detailed results are saved in `eval_results/{TICKER}/TradingAgentsStrategy_logs/`.
|
||||||
|
|
@ -0,0 +1,324 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""CLI bridge between Claude Code subagents and TradingAgents data/memory infrastructure.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python cc_tools.py <command> [args...]
|
||||||
|
|
||||||
|
Data Commands:
|
||||||
|
get_stock_data <symbol> <start_date> <end_date>
|
||||||
|
get_indicators <symbol> <indicator> <curr_date> [look_back_days]
|
||||||
|
get_fundamentals <ticker> <curr_date>
|
||||||
|
get_balance_sheet <ticker> [freq] [curr_date]
|
||||||
|
get_cashflow <ticker> [freq] [curr_date]
|
||||||
|
get_income_statement <ticker> [freq] [curr_date]
|
||||||
|
get_news <ticker> <start_date> <end_date>
|
||||||
|
get_global_news <curr_date> [look_back_days] [limit]
|
||||||
|
get_insider_transactions <ticker>
|
||||||
|
|
||||||
|
Memory Commands:
|
||||||
|
memory_get <memory_name> <situation_file> [n_matches]
|
||||||
|
memory_add <memory_name> <situation_file> <advice_file>
|
||||||
|
memory_clear <memory_name>
|
||||||
|
|
||||||
|
Results Commands:
|
||||||
|
save_results <ticker> <trade_date> <state_json_file>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _init_config():
|
||||||
|
"""Initialize the data vendor configuration."""
|
||||||
|
from tradingagents.default_config import DEFAULT_CONFIG
|
||||||
|
from tradingagents.dataflows.config import set_config
|
||||||
|
set_config(DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
# Create data cache directory
|
||||||
|
os.makedirs(
|
||||||
|
os.path.join(DEFAULT_CONFIG["project_dir"], "dataflows/data_cache"),
|
||||||
|
exist_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _route(method, *args, **kwargs):
|
||||||
|
"""Route a method call through the vendor interface."""
|
||||||
|
_init_config()
|
||||||
|
from tradingagents.dataflows.interface import route_to_vendor
|
||||||
|
return route_to_vendor(method, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Memory persistence helpers ---
|
||||||
|
|
||||||
|
MEMORY_DIR = Path("eval_results/.memory")
|
||||||
|
|
||||||
|
|
||||||
|
def _memory_path(name: str) -> Path:
|
||||||
|
return MEMORY_DIR / f"{name}.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_memory(name: str):
|
||||||
|
"""Load a FinancialSituationMemory from disk, or create a fresh one."""
|
||||||
|
from tradingagents.agents.utils.memory import FinancialSituationMemory
|
||||||
|
|
||||||
|
mem = FinancialSituationMemory(name)
|
||||||
|
path = _memory_path(name)
|
||||||
|
if path.exists():
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
docs = data.get("documents", [])
|
||||||
|
recs = data.get("recommendations", [])
|
||||||
|
if docs and recs and len(docs) == len(recs):
|
||||||
|
mem.add_situations(list(zip(docs, recs)))
|
||||||
|
return mem
|
||||||
|
|
||||||
|
|
||||||
|
def _save_memory(name: str, mem):
|
||||||
|
"""Persist a FinancialSituationMemory to disk."""
|
||||||
|
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
path = _memory_path(name)
|
||||||
|
data = {
|
||||||
|
"documents": mem.documents,
|
||||||
|
"recommendations": mem.recommendations,
|
||||||
|
}
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Command handlers ---
|
||||||
|
|
||||||
|
def cmd_get_stock_data(args):
|
||||||
|
result = _route("get_stock_data", args.symbol, args.start_date, args.end_date)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get_indicators(args):
|
||||||
|
look_back = int(args.look_back_days) if args.look_back_days else 30
|
||||||
|
result = _route("get_indicators", args.symbol, args.indicator, args.curr_date, look_back)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get_fundamentals(args):
|
||||||
|
result = _route("get_fundamentals", args.ticker, args.curr_date)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get_balance_sheet(args):
|
||||||
|
freq = args.freq or "quarterly"
|
||||||
|
curr_date = args.curr_date or None
|
||||||
|
result = _route("get_balance_sheet", args.ticker, freq, curr_date)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get_cashflow(args):
|
||||||
|
freq = args.freq or "quarterly"
|
||||||
|
curr_date = args.curr_date or None
|
||||||
|
result = _route("get_cashflow", args.ticker, freq, curr_date)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get_income_statement(args):
|
||||||
|
freq = args.freq or "quarterly"
|
||||||
|
curr_date = args.curr_date or None
|
||||||
|
result = _route("get_income_statement", args.ticker, freq, curr_date)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get_news(args):
|
||||||
|
result = _route("get_news", args.ticker, args.start_date, args.end_date)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get_global_news(args):
|
||||||
|
look_back = int(args.look_back_days) if args.look_back_days else 7
|
||||||
|
limit = int(args.limit) if args.limit else 5
|
||||||
|
result = _route("get_global_news", args.curr_date, look_back, limit)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get_insider_transactions(args):
|
||||||
|
result = _route("get_insider_transactions", args.ticker)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_memory_get(args):
|
||||||
|
situation_text = Path(args.situation_file).read_text(encoding="utf-8")
|
||||||
|
n_matches = int(args.n_matches) if args.n_matches else 2
|
||||||
|
|
||||||
|
mem = _load_memory(args.memory_name)
|
||||||
|
results = mem.get_memories(situation_text, n_matches=n_matches)
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
print("No past memories found.")
|
||||||
|
else:
|
||||||
|
for i, rec in enumerate(results, 1):
|
||||||
|
print(f"--- Memory Match {i} (score: {rec['similarity_score']:.2f}) ---")
|
||||||
|
print(rec["recommendation"])
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_memory_add(args):
|
||||||
|
situation_text = Path(args.situation_file).read_text(encoding="utf-8")
|
||||||
|
advice_text = Path(args.advice_file).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
mem = _load_memory(args.memory_name)
|
||||||
|
mem.add_situations([(situation_text, advice_text)])
|
||||||
|
_save_memory(args.memory_name, mem)
|
||||||
|
print(f"Memory added to '{args.memory_name}'. Total entries: {len(mem.documents)}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_memory_clear(args):
|
||||||
|
path = _memory_path(args.memory_name)
|
||||||
|
if path.exists():
|
||||||
|
path.unlink()
|
||||||
|
print(f"Memory '{args.memory_name}' cleared.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_save_results(args):
|
||||||
|
state_data = json.loads(Path(args.state_json_file).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
log_entry = {
|
||||||
|
str(args.trade_date): {
|
||||||
|
"company_of_interest": state_data.get("company_of_interest", args.ticker),
|
||||||
|
"trade_date": args.trade_date,
|
||||||
|
"market_report": state_data.get("market_report", ""),
|
||||||
|
"sentiment_report": state_data.get("sentiment_report", ""),
|
||||||
|
"news_report": state_data.get("news_report", ""),
|
||||||
|
"fundamentals_report": state_data.get("fundamentals_report", ""),
|
||||||
|
"investment_debate_state": state_data.get("investment_debate_state", {}),
|
||||||
|
"trader_investment_decision": state_data.get("trader_investment_plan", ""),
|
||||||
|
"risk_debate_state": state_data.get("risk_debate_state", {}),
|
||||||
|
"investment_plan": state_data.get("investment_plan", ""),
|
||||||
|
"final_trade_decision": state_data.get("final_trade_decision", ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
directory = Path(f"eval_results/{args.ticker}/TradingAgentsStrategy_logs/")
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
out_path = directory / f"full_states_log_{args.trade_date}.json"
|
||||||
|
with open(out_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(log_entry, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
print(f"Results saved to {out_path}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Argument parser ---
|
||||||
|
|
||||||
|
def build_parser():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="TradingAgents CLI tools for Claude Code subagents",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||||
|
|
||||||
|
# get_stock_data
|
||||||
|
p = subparsers.add_parser("get_stock_data", help="Get OHLCV stock price data")
|
||||||
|
p.add_argument("symbol", help="Ticker symbol (e.g., AAPL)")
|
||||||
|
p.add_argument("start_date", help="Start date (yyyy-mm-dd)")
|
||||||
|
p.add_argument("end_date", help="End date (yyyy-mm-dd)")
|
||||||
|
p.set_defaults(func=cmd_get_stock_data)
|
||||||
|
|
||||||
|
# get_indicators
|
||||||
|
p = subparsers.add_parser("get_indicators", help="Get technical indicators")
|
||||||
|
p.add_argument("symbol", help="Ticker symbol")
|
||||||
|
p.add_argument("indicator", help="Indicator name (e.g., rsi, macd)")
|
||||||
|
p.add_argument("curr_date", help="Current trading date (yyyy-mm-dd)")
|
||||||
|
p.add_argument("look_back_days", nargs="?", default=None, help="Lookback days (default: 30)")
|
||||||
|
p.set_defaults(func=cmd_get_indicators)
|
||||||
|
|
||||||
|
# get_fundamentals
|
||||||
|
p = subparsers.add_parser("get_fundamentals", help="Get company fundamentals")
|
||||||
|
p.add_argument("ticker", help="Ticker symbol")
|
||||||
|
p.add_argument("curr_date", help="Current date (yyyy-mm-dd)")
|
||||||
|
p.set_defaults(func=cmd_get_fundamentals)
|
||||||
|
|
||||||
|
# get_balance_sheet
|
||||||
|
p = subparsers.add_parser("get_balance_sheet", help="Get balance sheet data")
|
||||||
|
p.add_argument("ticker", help="Ticker symbol")
|
||||||
|
p.add_argument("freq", nargs="?", default=None, help="Frequency: annual/quarterly (default: quarterly)")
|
||||||
|
p.add_argument("curr_date", nargs="?", default=None, help="Current date (yyyy-mm-dd)")
|
||||||
|
p.set_defaults(func=cmd_get_balance_sheet)
|
||||||
|
|
||||||
|
# get_cashflow
|
||||||
|
p = subparsers.add_parser("get_cashflow", help="Get cash flow statement")
|
||||||
|
p.add_argument("ticker", help="Ticker symbol")
|
||||||
|
p.add_argument("freq", nargs="?", default=None, help="Frequency: annual/quarterly (default: quarterly)")
|
||||||
|
p.add_argument("curr_date", nargs="?", default=None, help="Current date (yyyy-mm-dd)")
|
||||||
|
p.set_defaults(func=cmd_get_cashflow)
|
||||||
|
|
||||||
|
# get_income_statement
|
||||||
|
p = subparsers.add_parser("get_income_statement", help="Get income statement")
|
||||||
|
p.add_argument("ticker", help="Ticker symbol")
|
||||||
|
p.add_argument("freq", nargs="?", default=None, help="Frequency: annual/quarterly (default: quarterly)")
|
||||||
|
p.add_argument("curr_date", nargs="?", default=None, help="Current date (yyyy-mm-dd)")
|
||||||
|
p.set_defaults(func=cmd_get_income_statement)
|
||||||
|
|
||||||
|
# get_news
|
||||||
|
p = subparsers.add_parser("get_news", help="Get company news")
|
||||||
|
p.add_argument("ticker", help="Ticker symbol")
|
||||||
|
p.add_argument("start_date", help="Start date (yyyy-mm-dd)")
|
||||||
|
p.add_argument("end_date", help="End date (yyyy-mm-dd)")
|
||||||
|
p.set_defaults(func=cmd_get_news)
|
||||||
|
|
||||||
|
# get_global_news
|
||||||
|
p = subparsers.add_parser("get_global_news", help="Get global macroeconomic news")
|
||||||
|
p.add_argument("curr_date", help="Current date (yyyy-mm-dd)")
|
||||||
|
p.add_argument("look_back_days", nargs="?", default=None, help="Lookback days (default: 7)")
|
||||||
|
p.add_argument("limit", nargs="?", default=None, help="Max articles (default: 5)")
|
||||||
|
p.set_defaults(func=cmd_get_global_news)
|
||||||
|
|
||||||
|
# get_insider_transactions
|
||||||
|
p = subparsers.add_parser("get_insider_transactions", help="Get insider transactions")
|
||||||
|
p.add_argument("ticker", help="Ticker symbol")
|
||||||
|
p.set_defaults(func=cmd_get_insider_transactions)
|
||||||
|
|
||||||
|
# memory_get
|
||||||
|
p = subparsers.add_parser("memory_get", help="Retrieve memories matching a situation")
|
||||||
|
p.add_argument("memory_name", help="Memory name (e.g., bull_memory)")
|
||||||
|
p.add_argument("situation_file", help="Path to file containing the situation text")
|
||||||
|
p.add_argument("n_matches", nargs="?", default=None, help="Number of matches (default: 2)")
|
||||||
|
p.set_defaults(func=cmd_memory_get)
|
||||||
|
|
||||||
|
# memory_add
|
||||||
|
p = subparsers.add_parser("memory_add", help="Add a situation+advice to memory")
|
||||||
|
p.add_argument("memory_name", help="Memory name (e.g., bull_memory)")
|
||||||
|
p.add_argument("situation_file", help="Path to file containing the situation text")
|
||||||
|
p.add_argument("advice_file", help="Path to file containing the advice text")
|
||||||
|
p.set_defaults(func=cmd_memory_add)
|
||||||
|
|
||||||
|
# memory_clear
|
||||||
|
p = subparsers.add_parser("memory_clear", help="Clear all entries from a memory")
|
||||||
|
p.add_argument("memory_name", help="Memory name to clear")
|
||||||
|
p.set_defaults(func=cmd_memory_clear)
|
||||||
|
|
||||||
|
# save_results
|
||||||
|
p = subparsers.add_parser("save_results", help="Save analysis results to JSON log")
|
||||||
|
p.add_argument("ticker", help="Ticker symbol")
|
||||||
|
p.add_argument("trade_date", help="Trade date (yyyy-mm-dd)")
|
||||||
|
p.add_argument("state_json_file", help="Path to JSON file containing the full state")
|
||||||
|
p.set_defaults(func=cmd_save_results)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = build_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.command:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
args.func(args)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
"""Integration tests for the Claude Code TradingAgents pipeline.
|
||||||
|
|
||||||
|
These tests verify that the full data pipeline works end-to-end,
|
||||||
|
including data fetching, memory persistence, and results saving.
|
||||||
|
|
||||||
|
This does NOT test the actual Claude Code subagent orchestration
|
||||||
|
(which requires a running Claude Code session), but verifies all
|
||||||
|
the infrastructure that subagents depend on.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
|
CC_TOOLS = PROJECT_ROOT / "cc_tools.py"
|
||||||
|
|
||||||
|
PYTHON = str(PROJECT_ROOT / ".venv" / "Scripts" / "python.exe")
|
||||||
|
if not Path(PYTHON).exists():
|
||||||
|
PYTHON = str(PROJECT_ROOT / ".venv" / "bin" / "python")
|
||||||
|
if not Path(PYTHON).exists():
|
||||||
|
PYTHON = sys.executable
|
||||||
|
|
||||||
|
|
||||||
|
def run(*args, timeout=60):
|
||||||
|
result = subprocess.run(
|
||||||
|
[PYTHON, str(CC_TOOLS)] + list(args),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
cwd=str(PROJECT_ROOT),
|
||||||
|
)
|
||||||
|
return result.stdout, result.stderr, result.returncode
|
||||||
|
|
||||||
|
|
||||||
|
class TestFullAnalystDataPipeline:
|
||||||
|
"""Simulate what the 4 analyst subagents would do: fetch all data for a ticker."""
|
||||||
|
|
||||||
|
TICKER = "MSFT"
|
||||||
|
TRADE_DATE = "2025-03-15"
|
||||||
|
START_30D = "2025-02-13"
|
||||||
|
START_7D = "2025-03-08"
|
||||||
|
|
||||||
|
def test_market_analyst_data(self):
|
||||||
|
"""Market analyst fetches stock data + indicators."""
|
||||||
|
# Stock data
|
||||||
|
stdout, _, rc = run("get_stock_data", self.TICKER, self.START_30D, self.TRADE_DATE)
|
||||||
|
assert rc == 0
|
||||||
|
assert "Open" in stdout or "Close" in stdout
|
||||||
|
stock_lines = [l for l in stdout.strip().split("\n") if not l.startswith("#")]
|
||||||
|
assert len(stock_lines) >= 2 # header + data
|
||||||
|
|
||||||
|
# Indicators
|
||||||
|
for indicator in ["rsi", "macd", "close_50_sma"]:
|
||||||
|
stdout, _, rc = run("get_indicators", self.TICKER, indicator, self.TRADE_DATE)
|
||||||
|
assert rc == 0
|
||||||
|
assert len(stdout.strip()) > 10
|
||||||
|
|
||||||
|
def test_social_analyst_data(self):
|
||||||
|
"""Social media analyst fetches company news."""
|
||||||
|
stdout, _, rc = run("get_news", self.TICKER, self.START_7D, self.TRADE_DATE)
|
||||||
|
assert rc == 0 # may have no news but should not crash
|
||||||
|
|
||||||
|
def test_news_analyst_data(self):
|
||||||
|
"""News analyst fetches company + global news."""
|
||||||
|
stdout, _, rc = run("get_news", self.TICKER, self.START_7D, self.TRADE_DATE)
|
||||||
|
assert rc == 0
|
||||||
|
|
||||||
|
stdout, _, rc = run("get_global_news", self.TRADE_DATE, "7", "5")
|
||||||
|
assert rc == 0
|
||||||
|
|
||||||
|
def test_fundamentals_analyst_data(self):
|
||||||
|
"""Fundamentals analyst fetches all financial statements."""
|
||||||
|
stdout, _, rc = run("get_fundamentals", self.TICKER, self.TRADE_DATE)
|
||||||
|
assert rc == 0
|
||||||
|
assert len(stdout.strip()) > 50
|
||||||
|
|
||||||
|
stdout, _, rc = run("get_balance_sheet", self.TICKER, "quarterly", self.TRADE_DATE)
|
||||||
|
assert rc == 0
|
||||||
|
|
||||||
|
stdout, _, rc = run("get_cashflow", self.TICKER, "quarterly", self.TRADE_DATE)
|
||||||
|
assert rc == 0
|
||||||
|
|
||||||
|
stdout, _, rc = run("get_income_statement", self.TICKER, "quarterly", self.TRADE_DATE)
|
||||||
|
assert rc == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestMemoryPersistenceAcrossInvocations:
|
||||||
|
"""Verify memory works across separate CLI invocations (simulating separate subagents)."""
|
||||||
|
|
||||||
|
MEMORY_NAME = "integration_test_memory"
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def cleanup(self):
|
||||||
|
run("memory_clear", self.MEMORY_NAME)
|
||||||
|
yield
|
||||||
|
run("memory_clear", self.MEMORY_NAME)
|
||||||
|
|
||||||
|
def test_memory_persists_across_calls(self, tmp_path):
|
||||||
|
"""Add memory in one call, retrieve in another — simulates cross-agent persistence."""
|
||||||
|
# First invocation: add a memory
|
||||||
|
sit = tmp_path / "sit.txt"
|
||||||
|
adv = tmp_path / "adv.txt"
|
||||||
|
sit.write_text(
|
||||||
|
"AAPL showing strong growth in services revenue with expanding margins. "
|
||||||
|
"iPhone sales declining but offset by services and wearables growth."
|
||||||
|
)
|
||||||
|
adv.write_text(
|
||||||
|
"The bull case was correct. Services growth proved more durable than expected. "
|
||||||
|
"Lesson: Don't underweight services revenue growth trajectory."
|
||||||
|
)
|
||||||
|
stdout, _, rc = run("memory_add", self.MEMORY_NAME, str(sit), str(adv))
|
||||||
|
assert rc == 0
|
||||||
|
|
||||||
|
# Second invocation: add another memory
|
||||||
|
sit2 = tmp_path / "sit2.txt"
|
||||||
|
adv2 = tmp_path / "adv2.txt"
|
||||||
|
sit2.write_text(
|
||||||
|
"NVDA GPU demand surging due to AI infrastructure buildout. "
|
||||||
|
"Data center revenue growing 200% year over year."
|
||||||
|
)
|
||||||
|
adv2.write_text(
|
||||||
|
"The aggressive stance was justified. AI infrastructure spend continued. "
|
||||||
|
"Lesson: When there's a genuine paradigm shift, be more aggressive."
|
||||||
|
)
|
||||||
|
stdout, _, rc = run("memory_add", self.MEMORY_NAME, str(sit2), str(adv2))
|
||||||
|
assert rc == 0
|
||||||
|
assert "Total entries: 2" in stdout
|
||||||
|
|
||||||
|
# Third invocation: query for similar situations
|
||||||
|
query = tmp_path / "query.txt"
|
||||||
|
query.write_text(
|
||||||
|
"Apple services segment showing accelerating growth while hardware sales plateau."
|
||||||
|
)
|
||||||
|
stdout, _, rc = run("memory_get", self.MEMORY_NAME, str(query), "2")
|
||||||
|
assert rc == 0
|
||||||
|
assert "Memory Match 1" in stdout
|
||||||
|
assert "Memory Match 2" in stdout
|
||||||
|
|
||||||
|
|
||||||
|
class TestEndToEndResultsSaving:
|
||||||
|
"""Test the full results save/load cycle."""
|
||||||
|
|
||||||
|
def test_save_and_verify_structure(self, tmp_path):
|
||||||
|
"""Verify saved results match the original TradingAgentsGraph._log_state format."""
|
||||||
|
state = {
|
||||||
|
"company_of_interest": "INTEGRATION_TEST",
|
||||||
|
"trade_date": "2025-03-15",
|
||||||
|
"market_report": "Market is trending upward with strong momentum indicators.",
|
||||||
|
"sentiment_report": "Social media sentiment is overwhelmingly positive.",
|
||||||
|
"news_report": "Recent earnings beat expectations. Fed holds rates steady.",
|
||||||
|
"fundamentals_report": "Strong balance sheet with growing free cash flow.",
|
||||||
|
"investment_debate_state": {
|
||||||
|
"bull_history": "Bull Analyst: Strong growth trajectory...",
|
||||||
|
"bear_history": "Bear Analyst: Overvalued at current levels...",
|
||||||
|
"history": "Bull Analyst: Strong growth...\nBear Analyst: Overvalued...",
|
||||||
|
"current_response": "Bear Analyst: Overvalued at current levels...",
|
||||||
|
"judge_decision": "Buy - bull case is more compelling",
|
||||||
|
},
|
||||||
|
"investment_plan": "Buy with a 12-month horizon, position size 5% of portfolio.",
|
||||||
|
"trader_investment_plan": "FINAL TRANSACTION PROPOSAL: **BUY**",
|
||||||
|
"risk_debate_state": {
|
||||||
|
"aggressive_history": "Aggressive: Go all in...",
|
||||||
|
"conservative_history": "Conservative: Limit to 3%...",
|
||||||
|
"neutral_history": "Neutral: 5% seems right...",
|
||||||
|
"history": "Aggressive: Go all in...\nConservative: Limit...\nNeutral: 5%...",
|
||||||
|
"judge_decision": "Buy with 5% position size, stop loss at -10%",
|
||||||
|
},
|
||||||
|
"final_trade_decision": "**Buy** - Position size: 5% of portfolio. Stop loss: -10%.",
|
||||||
|
}
|
||||||
|
|
||||||
|
state_file = tmp_path / "state.json"
|
||||||
|
state_file.write_text(json.dumps(state))
|
||||||
|
|
||||||
|
stdout, _, rc = run("save_results", "INTEGRATION_TEST", "2025-03-15", str(state_file))
|
||||||
|
assert rc == 0
|
||||||
|
assert "Results saved" in stdout
|
||||||
|
|
||||||
|
# Verify file structure matches original format
|
||||||
|
out_path = (
|
||||||
|
PROJECT_ROOT
|
||||||
|
/ "eval_results"
|
||||||
|
/ "INTEGRATION_TEST"
|
||||||
|
/ "TradingAgentsStrategy_logs"
|
||||||
|
/ "full_states_log_2025-03-15.json"
|
||||||
|
)
|
||||||
|
assert out_path.exists()
|
||||||
|
|
||||||
|
with open(out_path) as f:
|
||||||
|
saved = json.load(f)
|
||||||
|
|
||||||
|
entry = saved["2025-03-15"]
|
||||||
|
|
||||||
|
# Verify all expected fields exist (matching TradingAgentsGraph._log_state)
|
||||||
|
assert entry["company_of_interest"] == "INTEGRATION_TEST"
|
||||||
|
assert entry["trade_date"] == "2025-03-15"
|
||||||
|
assert "market_report" in entry
|
||||||
|
assert "sentiment_report" in entry
|
||||||
|
assert "news_report" in entry
|
||||||
|
assert "fundamentals_report" in entry
|
||||||
|
assert "investment_debate_state" in entry
|
||||||
|
assert "investment_plan" in entry
|
||||||
|
assert "final_trade_decision" in entry
|
||||||
|
assert "risk_debate_state" in entry
|
||||||
|
assert "trader_investment_decision" in entry
|
||||||
|
|
||||||
|
# Verify nested structure
|
||||||
|
assert "bull_history" in entry["investment_debate_state"]
|
||||||
|
assert "bear_history" in entry["investment_debate_state"]
|
||||||
|
assert "judge_decision" in entry["investment_debate_state"]
|
||||||
|
assert "aggressive_history" in entry["risk_debate_state"]
|
||||||
|
assert "conservative_history" in entry["risk_debate_state"]
|
||||||
|
assert "neutral_history" in entry["risk_debate_state"]
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
out_path.unlink()
|
||||||
|
out_path.parent.rmdir()
|
||||||
|
(PROJECT_ROOT / "eval_results" / "INTEGRATION_TEST").rmdir()
|
||||||
|
|
@ -0,0 +1,307 @@
|
||||||
|
"""Tests for cc_tools.py CLI bridge.
|
||||||
|
|
||||||
|
Each test runs cc_tools.py as a subprocess to verify it works correctly
|
||||||
|
when called from Claude Code subagents via Bash.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Find the project root (where cc_tools.py lives)
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
|
CC_TOOLS = PROJECT_ROOT / "cc_tools.py"
|
||||||
|
|
||||||
|
# Find Python executable - prefer venv
|
||||||
|
PYTHON = str(PROJECT_ROOT / ".venv" / "Scripts" / "python.exe")
|
||||||
|
if not Path(PYTHON).exists():
|
||||||
|
PYTHON = str(PROJECT_ROOT / ".venv" / "bin" / "python")
|
||||||
|
if not Path(PYTHON).exists():
|
||||||
|
PYTHON = sys.executable
|
||||||
|
|
||||||
|
|
||||||
|
def run_cc_tools(*args, timeout=60):
|
||||||
|
"""Run cc_tools.py with given arguments and return (stdout, stderr, returncode)."""
|
||||||
|
cmd = [PYTHON, str(CC_TOOLS)] + list(args)
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
cwd=str(PROJECT_ROOT),
|
||||||
|
)
|
||||||
|
return result.stdout, result.stderr, result.returncode
|
||||||
|
|
||||||
|
|
||||||
|
class TestHelp:
|
||||||
|
def test_help_output(self):
|
||||||
|
stdout, stderr, rc = run_cc_tools("--help")
|
||||||
|
assert rc == 0
|
||||||
|
assert "get_stock_data" in stdout
|
||||||
|
assert "memory_get" in stdout
|
||||||
|
assert "save_results" in stdout
|
||||||
|
|
||||||
|
def test_no_args_shows_help(self):
|
||||||
|
stdout, stderr, rc = run_cc_tools()
|
||||||
|
assert rc == 1 # should fail with no command
|
||||||
|
|
||||||
|
|
||||||
|
class TestStockData:
|
||||||
|
def test_get_stock_data_returns_csv(self):
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"get_stock_data", "AAPL", "2025-03-01", "2025-03-15"
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
assert "AAPL" in stdout or "Date" in stdout or "Open" in stdout
|
||||||
|
# Should contain CSV-like data
|
||||||
|
lines = stdout.strip().split("\n")
|
||||||
|
assert len(lines) > 2 # header + at least one data row
|
||||||
|
|
||||||
|
def test_get_stock_data_invalid_ticker(self):
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"get_stock_data", "INVALIDTICKER12345", "2025-03-01", "2025-03-15"
|
||||||
|
)
|
||||||
|
# Should either return empty/error but not crash
|
||||||
|
assert rc == 0 or rc == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestIndicators:
|
||||||
|
def test_get_rsi(self):
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"get_indicators", "AAPL", "rsi", "2025-03-15"
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
assert "rsi" in stdout.lower()
|
||||||
|
|
||||||
|
def test_get_macd(self):
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"get_indicators", "AAPL", "macd", "2025-03-15"
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
assert "macd" in stdout.lower()
|
||||||
|
|
||||||
|
def test_get_indicators_with_lookback(self):
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"get_indicators", "AAPL", "rsi", "2025-03-15", "15"
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestFundamentals:
|
||||||
|
def test_get_fundamentals(self):
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"get_fundamentals", "AAPL", "2025-03-15"
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
assert len(stdout.strip()) > 50 # should have substantial content
|
||||||
|
# Should contain some fundamental data keywords
|
||||||
|
assert any(
|
||||||
|
kw in stdout.lower()
|
||||||
|
for kw in ["market cap", "pe ratio", "eps", "sector", "apple"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_balance_sheet(self):
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"get_balance_sheet", "AAPL"
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
assert len(stdout.strip()) > 20
|
||||||
|
|
||||||
|
def test_get_cashflow(self):
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"get_cashflow", "AAPL"
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
assert len(stdout.strip()) > 20
|
||||||
|
|
||||||
|
def test_get_income_statement(self):
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"get_income_statement", "AAPL"
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
assert len(stdout.strip()) > 20
|
||||||
|
|
||||||
|
|
||||||
|
class TestNews:
|
||||||
|
def test_get_news(self):
|
||||||
|
# Use recent dates for better chance of finding news
|
||||||
|
end = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
start = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"get_news", "AAPL", start, end
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
# May or may not find news, but should not crash
|
||||||
|
|
||||||
|
def test_get_global_news(self):
|
||||||
|
curr = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"get_global_news", curr
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
|
||||||
|
def test_get_insider_transactions(self):
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"get_insider_transactions", "AAPL"
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestMemory:
|
||||||
|
"""Test memory persistence (add, get, clear)."""
|
||||||
|
|
||||||
|
MEMORY_NAME = "pytest_test_memory"
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def cleanup_memory(self):
|
||||||
|
"""Clean up test memory before and after each test."""
|
||||||
|
run_cc_tools("memory_clear", self.MEMORY_NAME)
|
||||||
|
yield
|
||||||
|
run_cc_tools("memory_clear", self.MEMORY_NAME)
|
||||||
|
|
||||||
|
def test_memory_add_and_get_roundtrip(self, tmp_path):
|
||||||
|
# Write situation and advice to temp files
|
||||||
|
sit_file = tmp_path / "situation.txt"
|
||||||
|
adv_file = tmp_path / "advice.txt"
|
||||||
|
sit_file.write_text("High inflation with rising interest rates affecting tech stocks")
|
||||||
|
adv_file.write_text("Consider defensive sectors like utilities and consumer staples")
|
||||||
|
|
||||||
|
# Add to memory
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"memory_add", self.MEMORY_NAME,
|
||||||
|
str(sit_file), str(adv_file)
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
assert "Memory added" in stdout
|
||||||
|
assert "Total entries: 1" in stdout
|
||||||
|
|
||||||
|
# Query memory
|
||||||
|
query_file = tmp_path / "query.txt"
|
||||||
|
query_file.write_text("Rising rates impacting technology sector valuations")
|
||||||
|
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"memory_get", self.MEMORY_NAME,
|
||||||
|
str(query_file), "1"
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
assert "defensive sectors" in stdout.lower() or "utilities" in stdout.lower()
|
||||||
|
|
||||||
|
def test_memory_get_empty(self, tmp_path):
|
||||||
|
query_file = tmp_path / "query.txt"
|
||||||
|
query_file.write_text("Some query text")
|
||||||
|
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"memory_get", self.MEMORY_NAME,
|
||||||
|
str(query_file), "1"
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
assert "No past memories found" in stdout
|
||||||
|
|
||||||
|
def test_memory_multiple_entries(self, tmp_path):
|
||||||
|
# Add two entries
|
||||||
|
for i, (sit, adv) in enumerate([
|
||||||
|
("Bull market with tech sector leading gains", "Increase tech allocation"),
|
||||||
|
("Bear market with economic recession fears", "Reduce equity exposure"),
|
||||||
|
]):
|
||||||
|
sit_file = tmp_path / f"sit_{i}.txt"
|
||||||
|
adv_file = tmp_path / f"adv_{i}.txt"
|
||||||
|
sit_file.write_text(sit)
|
||||||
|
adv_file.write_text(adv)
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"memory_add", self.MEMORY_NAME,
|
||||||
|
str(sit_file), str(adv_file)
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
|
||||||
|
# Query for something tech-related
|
||||||
|
query_file = tmp_path / "query.txt"
|
||||||
|
query_file.write_text("Tech stocks surging in bull market conditions")
|
||||||
|
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"memory_get", self.MEMORY_NAME,
|
||||||
|
str(query_file), "2"
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
assert "Memory Match 1" in stdout
|
||||||
|
assert "Memory Match 2" in stdout
|
||||||
|
|
||||||
|
def test_memory_clear(self, tmp_path):
|
||||||
|
# Add entry
|
||||||
|
sit_file = tmp_path / "sit.txt"
|
||||||
|
adv_file = tmp_path / "adv.txt"
|
||||||
|
sit_file.write_text("test situation")
|
||||||
|
adv_file.write_text("test advice")
|
||||||
|
run_cc_tools("memory_add", self.MEMORY_NAME, str(sit_file), str(adv_file))
|
||||||
|
|
||||||
|
# Clear
|
||||||
|
stdout, stderr, rc = run_cc_tools("memory_clear", self.MEMORY_NAME)
|
||||||
|
assert rc == 0
|
||||||
|
assert "cleared" in stdout.lower()
|
||||||
|
|
||||||
|
# Verify empty
|
||||||
|
query_file = tmp_path / "query.txt"
|
||||||
|
query_file.write_text("test")
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"memory_get", self.MEMORY_NAME, str(query_file)
|
||||||
|
)
|
||||||
|
assert "No past memories found" in stdout
|
||||||
|
|
||||||
|
|
||||||
|
class TestSaveResults:
|
||||||
|
def test_save_results(self, tmp_path):
|
||||||
|
state = {
|
||||||
|
"company_of_interest": "TEST",
|
||||||
|
"trade_date": "2025-03-15",
|
||||||
|
"market_report": "Test market report",
|
||||||
|
"sentiment_report": "Test sentiment report",
|
||||||
|
"news_report": "Test news report",
|
||||||
|
"fundamentals_report": "Test fundamentals report",
|
||||||
|
"investment_debate_state": {
|
||||||
|
"bull_history": "Bull argument",
|
||||||
|
"bear_history": "Bear argument",
|
||||||
|
"history": "Full debate",
|
||||||
|
"current_response": "Latest",
|
||||||
|
"judge_decision": "Buy",
|
||||||
|
},
|
||||||
|
"trader_investment_plan": "Buy ASAP",
|
||||||
|
"risk_debate_state": {
|
||||||
|
"aggressive_history": "Go big",
|
||||||
|
"conservative_history": "Be cautious",
|
||||||
|
"neutral_history": "Balance",
|
||||||
|
"history": "Full risk debate",
|
||||||
|
"judge_decision": "Buy with limits",
|
||||||
|
},
|
||||||
|
"investment_plan": "Investment plan text",
|
||||||
|
"final_trade_decision": "Buy",
|
||||||
|
}
|
||||||
|
|
||||||
|
state_file = tmp_path / "state.json"
|
||||||
|
state_file.write_text(json.dumps(state))
|
||||||
|
|
||||||
|
stdout, stderr, rc = run_cc_tools(
|
||||||
|
"save_results", "TEST", "2025-03-15", str(state_file)
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
assert "Results saved" in stdout
|
||||||
|
|
||||||
|
# Verify the output file exists
|
||||||
|
out_path = PROJECT_ROOT / "eval_results" / "TEST" / "TradingAgentsStrategy_logs" / "full_states_log_2025-03-15.json"
|
||||||
|
assert out_path.exists()
|
||||||
|
|
||||||
|
# Verify content
|
||||||
|
with open(out_path) as f:
|
||||||
|
saved = json.load(f)
|
||||||
|
assert "2025-03-15" in saved
|
||||||
|
assert saved["2025-03-15"]["market_report"] == "Test market report"
|
||||||
|
assert saved["2025-03-15"]["final_trade_decision"] == "Buy"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
out_path.unlink()
|
||||||
|
out_path.parent.rmdir()
|
||||||
|
(PROJECT_ROOT / "eval_results" / "TEST").rmdir()
|
||||||
Loading…
Reference in New Issue