+ )}
{/* How It Works Section */}
diff --git a/frontend/src/pages/StockDetail.tsx b/frontend/src/pages/StockDetail.tsx
index 2bc52e14..cdd18f12 100644
--- a/frontend/src/pages/StockDetail.tsx
+++ b/frontend/src/pages/StockDetail.tsx
@@ -18,6 +18,7 @@ import {
DataSourcesPanel
} from '../components/pipeline';
import { api } from '../services/api';
+import { useSettings } from '../contexts/SettingsContext';
import type { FullPipelineData, AgentType } from '../types/pipeline';
type TabType = 'overview' | 'pipeline' | 'debates' | 'data';
@@ -30,6 +31,7 @@ export default function StockDetail() {
const [isRefreshing, setIsRefreshing] = useState(false);
const [lastRefresh, setLastRefresh] = useState(null);
const [refreshMessage, setRefreshMessage] = useState(null);
+ const { settings } = useSettings();
// Analysis state
const [isAnalysisRunning, setIsAnalysisRunning] = useState(false);
@@ -116,8 +118,14 @@ export default function StockDetail() {
setAnalysisProgress('Starting analysis...');
try {
- // Trigger analysis
- await api.runAnalysis(symbol, latestRecommendation.date);
+ // Trigger analysis with settings from context
+ await api.runAnalysis(symbol, latestRecommendation.date, {
+ deep_think_model: settings.deepThinkModel,
+ quick_think_model: settings.quickThinkModel,
+ provider: settings.provider,
+ api_key: settings.provider === 'anthropic_api' ? settings.anthropicApiKey : undefined,
+ max_debate_rounds: settings.maxDebateRounds
+ });
setAnalysisStatus('running');
// Poll for status
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts
index b9201fc2..086cce82 100644
--- a/frontend/src/services/api.ts
+++ b/frontend/src/services/api.ts
@@ -70,6 +70,17 @@ export interface StockHistory {
risk?: string;
}
+/**
+ * Analysis configuration options
+ */
+export interface AnalysisConfig {
+ deep_think_model?: string;
+ quick_think_model?: string;
+ provider?: string;
+ api_key?: string;
+ max_debate_rounds?: number;
+}
+
class ApiService {
private baseUrl: string;
@@ -242,7 +253,7 @@ class ApiService {
/**
* Start analysis for a stock
*/
- async runAnalysis(symbol: string, date?: string): Promise<{
+ async runAnalysis(symbol: string, date?: string, config?: AnalysisConfig): Promise<{
message: string;
symbol: string;
date: string;
@@ -251,7 +262,7 @@ class ApiService {
const url = date ? `/analyze/${symbol}?date=${date}` : `/analyze/${symbol}`;
return this.fetch(url, {
method: 'POST',
- body: JSON.stringify({}),
+ body: JSON.stringify(config || {}),
noCache: true,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
@@ -284,6 +295,45 @@ class ApiService {
}> {
return this.fetch('/analyze/running', { noCache: true });
}
+
+ /**
+ * Start bulk analysis for all Nifty 50 stocks
+ */
+ async runBulkAnalysis(date?: string, config?: {
+ deep_think_model?: string;
+ quick_think_model?: string;
+ provider?: string;
+ api_key?: string;
+ max_debate_rounds?: number;
+ }): Promise<{
+ message: string;
+ date: string;
+ total_stocks: number;
+ status: string;
+ }> {
+ const url = date ? `/analyze/all?date=${date}` : '/analyze/all';
+ return this.fetch(url, {
+ method: 'POST',
+ body: JSON.stringify(config || {}),
+ noCache: true
+ });
+ }
+
+ /**
+ * Get bulk analysis status
+ */
+ async getBulkAnalysisStatus(): Promise<{
+ status: string;
+ total: number;
+ completed: number;
+ failed: number;
+ current_symbol: string | null;
+ started_at: string | null;
+ completed_at: string | null;
+ results: Record;
+ }> {
+ return this.fetch('/analyze/all/status', { noCache: true });
+ }
}
export const api = new ApiService();
diff --git a/frontend/src/types/pipeline.ts b/frontend/src/types/pipeline.ts
index 7354aa02..d59727b6 100644
--- a/frontend/src/types/pipeline.ts
+++ b/frontend/src/types/pipeline.ts
@@ -76,7 +76,7 @@ export interface PipelineStep {
export interface DataSourceLog {
source_type: string;
source_name: string;
- data_fetched?: Record;
+ data_fetched?: Record | string;
fetch_timestamp?: string;
success: boolean;
error_message?: string;
diff --git a/tradingagents/agents/utils/memory.py b/tradingagents/agents/utils/memory.py
index 04bf2c52..a1a7ba71 100644
--- a/tradingagents/agents/utils/memory.py
+++ b/tradingagents/agents/utils/memory.py
@@ -17,7 +17,8 @@ class FinancialSituationMemory:
# Use ChromaDB's default embedding function (uses all-MiniLM-L6-v2 internally)
self.embedding_fn = embedding_functions.DefaultEmbeddingFunction()
self.chroma_client = chromadb.Client(Settings(allow_reset=True))
- self.situation_collection = self.chroma_client.create_collection(
+ # Use get_or_create to avoid errors when collection already exists
+ self.situation_collection = self.chroma_client.get_or_create_collection(
name=name,
embedding_function=self.embedding_fn
)
diff --git a/tradingagents/claude_max_llm.py b/tradingagents/claude_max_llm.py
index a3e718ca..2eed4d87 100644
--- a/tradingagents/claude_max_llm.py
+++ b/tradingagents/claude_max_llm.py
@@ -8,7 +8,9 @@ with Max subscription authentication instead of API keys.
import os
import subprocess
import json
-from typing import Any, Dict, List, Optional, Iterator
+import re
+import copy
+from typing import Any, Dict, List, Optional, Iterator, Sequence, Union
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import (
@@ -16,9 +18,12 @@ from langchain_core.messages import (
BaseMessage,
HumanMessage,
SystemMessage,
+ ToolMessage,
)
from langchain_core.outputs import ChatGeneration, ChatResult
from langchain_core.callbacks import CallbackManagerForLLMRun
+from langchain_core.tools import BaseTool
+from langchain_core.runnables import Runnable
class ClaudeMaxLLM(BaseChatModel):
@@ -33,6 +38,10 @@ class ClaudeMaxLLM(BaseChatModel):
max_tokens: int = 4096
temperature: float = 0.7
claude_cli_path: str = "claude"
+ tools: List[Any] = [] # Bound tools
+
+ class Config:
+ arbitrary_types_allowed = True
@property
def _llm_type(self) -> str:
@@ -46,19 +55,94 @@ class ClaudeMaxLLM(BaseChatModel):
"temperature": self.temperature,
}
+ def bind_tools(
+ self,
+ tools: Sequence[Union[Dict[str, Any], BaseTool, Any]],
+ **kwargs: Any,
+ ) -> "ClaudeMaxLLM":
+ """Bind tools to the model for function calling.
+
+ Args:
+ tools: A list of tools to bind to the model.
+ **kwargs: Additional arguments (ignored for compatibility).
+
+ Returns:
+ A new ClaudeMaxLLM instance with tools bound.
+ """
+ # Create a copy with tools bound
+ new_instance = ClaudeMaxLLM(
+ model=self.model,
+ max_tokens=self.max_tokens,
+ temperature=self.temperature,
+ claude_cli_path=self.claude_cli_path,
+ tools=list(tools),
+ )
+ return new_instance
+
+ def _format_tools_for_prompt(self) -> str:
+ """Format bound tools as a string for the prompt."""
+ if not self.tools:
+ return ""
+
+ tool_descriptions = []
+ for tool in self.tools:
+ if hasattr(tool, 'name') and hasattr(tool, 'description'):
+ # LangChain BaseTool
+ name = tool.name
+ desc = tool.description
+ args = ""
+ if hasattr(tool, 'args_schema') and tool.args_schema:
+ schema = tool.args_schema.schema() if hasattr(tool.args_schema, 'schema') else {}
+ if 'properties' in schema:
+ args = ", ".join(f"{k}: {v.get('type', 'any')}" for k, v in schema['properties'].items())
+ tool_descriptions.append(f"- {name}({args}): {desc}")
+ elif isinstance(tool, dict):
+ # Dict format
+ name = tool.get('name', 'unknown')
+ desc = tool.get('description', '')
+ tool_descriptions.append(f"- {name}: {desc}")
+ else:
+ # Try to get function info
+ name = getattr(tool, '__name__', str(tool))
+ desc = getattr(tool, '__doc__', '') or ''
+ tool_descriptions.append(f"- {name}: {desc[:100]}")
+
+ return "\n\nAvailable tools:\n" + "\n".join(tool_descriptions) + "\n\nTo use a tool, respond with: TOOL_CALL: tool_name(arguments)\n"
+
def _format_messages_for_prompt(self, messages: List[BaseMessage]) -> str:
"""Convert LangChain messages to a single prompt string."""
formatted_parts = []
+ # Add tools description if tools are bound
+ tools_prompt = self._format_tools_for_prompt()
+ if tools_prompt:
+ formatted_parts.append(tools_prompt)
+
for msg in messages:
- if isinstance(msg, SystemMessage):
+ # Handle dict messages (LangChain sometimes passes these)
+ if isinstance(msg, dict):
+ role = msg.get("role", msg.get("type", "human"))
+ content = msg.get("content", str(msg))
+ if role in ("system",):
+ formatted_parts.append(f"\n{content}\n\n")
+ elif role in ("human", "user"):
+ formatted_parts.append(f"Human: {content}\n")
+ elif role in ("ai", "assistant"):
+ formatted_parts.append(f"Assistant: {content}\n")
+ else:
+ formatted_parts.append(f"{content}\n")
+ elif isinstance(msg, SystemMessage):
formatted_parts.append(f"\n{msg.content}\n\n")
elif isinstance(msg, HumanMessage):
formatted_parts.append(f"Human: {msg.content}\n")
elif isinstance(msg, AIMessage):
formatted_parts.append(f"Assistant: {msg.content}\n")
- else:
+ elif isinstance(msg, ToolMessage):
+ formatted_parts.append(f"Tool Result ({msg.name}): {msg.content}\n")
+ elif hasattr(msg, 'content'):
formatted_parts.append(f"{msg.content}\n")
+ else:
+ formatted_parts.append(f"{str(msg)}\n")
return "\n".join(formatted_parts)
@@ -68,12 +152,12 @@ class ClaudeMaxLLM(BaseChatModel):
env = os.environ.copy()
env.pop("ANTHROPIC_API_KEY", None)
- # Build the command
+ # Build the command - use --prompt flag with stdin for long prompts
cmd = [
self.claude_cli_path,
"--print", # Non-interactive mode
"--model", self.model,
- prompt
+ "-p", prompt # Use -p flag for prompt
]
try:
@@ -86,7 +170,9 @@ class ClaudeMaxLLM(BaseChatModel):
)
if result.returncode != 0:
- raise RuntimeError(f"Claude CLI error: {result.stderr}")
+ # Include both stdout and stderr for better debugging
+ error_info = result.stderr or result.stdout or "No output"
+ raise RuntimeError(f"Claude CLI error (code {result.returncode}): {error_info}")
return result.stdout.strip()
@@ -120,7 +206,14 @@ class ClaudeMaxLLM(BaseChatModel):
return ChatResult(generations=[generation])
- def invoke(self, input: Any, **kwargs) -> AIMessage:
+ def invoke(
+ self,
+ input: Any,
+ config: Optional[Dict[str, Any]] = None,
+ *,
+ stop: Optional[List[str]] = None,
+ **kwargs: Any
+ ) -> AIMessage:
"""Invoke the model with the given input."""
if isinstance(input, str):
messages = [HumanMessage(content=input)]
@@ -129,11 +222,11 @@ class ClaudeMaxLLM(BaseChatModel):
else:
messages = [HumanMessage(content=str(input))]
- result = self._generate(messages, **kwargs)
+ result = self._generate(messages, stop=stop, **kwargs)
return result.generations[0].message
-def get_claude_max_llm(model: str = "claude-sonnet-4-5-20250514", **kwargs) -> ClaudeMaxLLM:
+def get_claude_max_llm(model: str = "sonnet", **kwargs) -> ClaudeMaxLLM:
"""
Factory function to create a ClaudeMaxLLM instance.
@@ -151,7 +244,7 @@ def test_claude_max():
"""Test the Claude Max LLM wrapper."""
print("Testing Claude Max LLM wrapper...")
- llm = ClaudeMaxLLM(model="claude-sonnet-4-5-20250514")
+ llm = ClaudeMaxLLM(model="sonnet")
# Test with a simple prompt
response = llm.invoke("Say 'Hello, I am using Claude Max subscription!' in exactly those words.")
diff --git a/tradingagents/dataflows/alpha_vantage_fundamentals.py b/tradingagents/dataflows/alpha_vantage_fundamentals.py
index 8b92faa6..f8148df3 100644
--- a/tradingagents/dataflows/alpha_vantage_fundamentals.py
+++ b/tradingagents/dataflows/alpha_vantage_fundamentals.py
@@ -1,13 +1,78 @@
+from datetime import datetime, timedelta
from .alpha_vantage_common import _make_api_request
+import json
+
+
+def _filter_reports_by_date(data_str: str, curr_date: str, report_keys: list = None) -> str:
+ """
+ Filter Alpha Vantage fundamentals data to only include reports available as of curr_date.
+ This ensures point-in-time accuracy for backtesting.
+
+ Financial reports are typically published ~45 days after the fiscal date ending.
+ We filter to only include reports that would have been published by curr_date.
+
+ Args:
+ data_str: JSON string from Alpha Vantage API
+ curr_date: The backtest date in yyyy-mm-dd format
+ report_keys: List of keys containing report arrays (e.g., ['quarterlyReports', 'annualReports'])
+
+ Returns:
+ Filtered JSON string with only point-in-time available reports
+ """
+ if curr_date is None:
+ return data_str
+
+ if report_keys is None:
+ report_keys = ['quarterlyReports', 'annualReports']
+
+ try:
+ data = json.loads(data_str)
+ curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
+ # Financial reports typically published ~45 days after fiscal date ending
+ publication_delay_days = 45
+
+ for key in report_keys:
+ if key in data and isinstance(data[key], list):
+ filtered_reports = []
+ for report in data[key]:
+ fiscal_date = report.get('fiscalDateEnding')
+ if fiscal_date:
+ try:
+ fiscal_date_dt = datetime.strptime(fiscal_date, "%Y-%m-%d")
+ # Estimate when this report would have been published
+ estimated_publish_date = fiscal_date_dt + timedelta(days=publication_delay_days)
+ if estimated_publish_date <= curr_date_dt:
+ filtered_reports.append(report)
+ except ValueError:
+ # If date parsing fails, keep the report
+ filtered_reports.append(report)
+ else:
+ # If no fiscal date, keep the report
+ filtered_reports.append(report)
+ data[key] = filtered_reports
+
+ # Add point-in-time metadata
+ data['_point_in_time_date'] = curr_date
+ data['_filtered_for_backtesting'] = True
+
+ return json.dumps(data, indent=2)
+
+ except (json.JSONDecodeError, Exception) as e:
+ # If parsing fails, return original data with warning
+ print(f"Warning: Could not filter Alpha Vantage data by date: {e}")
+ return data_str
def get_fundamentals(ticker: str, curr_date: str = None) -> str:
"""
Retrieve comprehensive fundamental data for a given ticker symbol using Alpha Vantage.
+ Note: OVERVIEW endpoint returns current snapshot data only. For backtesting,
+ this may not reflect the exact fundamentals as of the historical date.
+
Args:
ticker (str): Ticker symbol of the company
- curr_date (str): Current date you are trading at, yyyy-mm-dd (not used for Alpha Vantage)
+ curr_date (str): Current date you are trading at, yyyy-mm-dd (used for documentation)
Returns:
str: Company overview data including financial ratios and key metrics
@@ -16,62 +81,91 @@ def get_fundamentals(ticker: str, curr_date: str = None) -> str:
"symbol": ticker,
}
- return _make_api_request("OVERVIEW", params)
+ result = _make_api_request("OVERVIEW", params)
+
+ # Add warning about point-in-time accuracy for OVERVIEW data
+ if curr_date and result and not result.startswith("Error"):
+ try:
+ data = json.loads(result)
+ data['_warning'] = (
+ "OVERVIEW data is current snapshot only. For accurate backtesting, "
+ "fundamental ratios may differ from actual values as of " + curr_date
+ )
+ data['_requested_date'] = curr_date
+ return json.dumps(data, indent=2)
+ except:
+ pass
+
+ return result
def get_balance_sheet(ticker: str, freq: str = "quarterly", curr_date: str = None) -> str:
"""
Retrieve balance sheet data for a given ticker symbol using Alpha Vantage.
+ Filtered by curr_date for point-in-time backtesting accuracy.
Args:
ticker (str): Ticker symbol of the company
- freq (str): Reporting frequency: annual/quarterly (default quarterly) - not used for Alpha Vantage
- curr_date (str): Current date you are trading at, yyyy-mm-dd (not used for Alpha Vantage)
+ freq (str): Reporting frequency: annual/quarterly (default quarterly)
+ curr_date (str): Current date you are trading at, yyyy-mm-dd (used for point-in-time filtering)
Returns:
- str: Balance sheet data with normalized fields
+ str: Balance sheet data with normalized fields, filtered to only include
+ reports that would have been published by curr_date
"""
params = {
"symbol": ticker,
}
- return _make_api_request("BALANCE_SHEET", params)
+ result = _make_api_request("BALANCE_SHEET", params)
+
+ # Filter reports to only include those available as of curr_date
+ return _filter_reports_by_date(result, curr_date)
def get_cashflow(ticker: str, freq: str = "quarterly", curr_date: str = None) -> str:
"""
Retrieve cash flow statement data for a given ticker symbol using Alpha Vantage.
+ Filtered by curr_date for point-in-time backtesting accuracy.
Args:
ticker (str): Ticker symbol of the company
- freq (str): Reporting frequency: annual/quarterly (default quarterly) - not used for Alpha Vantage
- curr_date (str): Current date you are trading at, yyyy-mm-dd (not used for Alpha Vantage)
+ freq (str): Reporting frequency: annual/quarterly (default quarterly)
+ curr_date (str): Current date you are trading at, yyyy-mm-dd (used for point-in-time filtering)
Returns:
- str: Cash flow statement data with normalized fields
+ str: Cash flow statement data with normalized fields, filtered to only include
+ reports that would have been published by curr_date
"""
params = {
"symbol": ticker,
}
- return _make_api_request("CASH_FLOW", params)
+ result = _make_api_request("CASH_FLOW", params)
+
+ # Filter reports to only include those available as of curr_date
+ return _filter_reports_by_date(result, curr_date)
def get_income_statement(ticker: str, freq: str = "quarterly", curr_date: str = None) -> str:
"""
Retrieve income statement data for a given ticker symbol using Alpha Vantage.
+ Filtered by curr_date for point-in-time backtesting accuracy.
Args:
ticker (str): Ticker symbol of the company
- freq (str): Reporting frequency: annual/quarterly (default quarterly) - not used for Alpha Vantage
- curr_date (str): Current date you are trading at, yyyy-mm-dd (not used for Alpha Vantage)
+ freq (str): Reporting frequency: annual/quarterly (default quarterly)
+ curr_date (str): Current date you are trading at, yyyy-mm-dd (used for point-in-time filtering)
Returns:
- str: Income statement data with normalized fields
+ str: Income statement data with normalized fields, filtered to only include
+ reports that would have been published by curr_date
"""
params = {
"symbol": ticker,
}
- return _make_api_request("INCOME_STATEMENT", params)
+ result = _make_api_request("INCOME_STATEMENT", params)
+ # Filter reports to only include those available as of curr_date
+ return _filter_reports_by_date(result, curr_date)
diff --git a/tradingagents/dataflows/y_finance.py b/tradingagents/dataflows/y_finance.py
index b6f109b3..6e5b1d5e 100644
--- a/tradingagents/dataflows/y_finance.py
+++ b/tradingagents/dataflows/y_finance.py
@@ -220,11 +220,12 @@ def _get_stock_stats_bulk(
raise Exception("Stockstats fail: Yahoo Finance data not fetched yet!")
else:
# Online data fetching with caching
- today_date = pd.Timestamp.today()
+ # IMPORTANT: Use curr_date as end_date for backtesting accuracy
+ # This ensures we only use data available at the backtest date (point-in-time)
curr_date_dt = pd.to_datetime(curr_date)
-
- end_date = today_date
- start_date = today_date - pd.DateOffset(years=15)
+
+ end_date = curr_date_dt # Use backtest date, NOT today's date
+ start_date = curr_date_dt - pd.DateOffset(years=15)
start_date_str = start_date.strftime("%Y-%m-%d")
end_date_str = end_date.strftime("%Y-%m-%d")
@@ -297,30 +298,80 @@ def get_stockstats_indicator(
return str(indicator_value)
+def _filter_fundamentals_by_date(data, curr_date):
+ """
+ Filter fundamentals data to only include reports available on or before curr_date.
+ This ensures point-in-time accuracy for backtesting.
+
+ yfinance returns fundamentals with report dates as column headers.
+ Financial reports are typically published 30-45 days after quarter end.
+ We filter to only include columns (report dates) that are at least 45 days before curr_date.
+ """
+ import pandas as pd
+
+ if data.empty or curr_date is None:
+ return data
+
+ try:
+ curr_date_dt = pd.to_datetime(curr_date)
+ # Financial reports are typically published ~45 days after the report date
+ # So for a report dated 2024-03-31, it would be available around mid-May
+ publication_delay_days = 45
+
+ # Filter columns (report dates) to only include those available at curr_date
+ valid_columns = []
+ for col in data.columns:
+ try:
+ report_date = pd.to_datetime(col)
+ # Report would have been published ~45 days after report_date
+ estimated_publish_date = report_date + pd.Timedelta(days=publication_delay_days)
+ if estimated_publish_date <= curr_date_dt:
+ valid_columns.append(col)
+ except:
+ # If column can't be parsed as date, keep it (might be a label column)
+ valid_columns.append(col)
+
+ if valid_columns:
+ return data[valid_columns]
+ else:
+ return data.iloc[:, :0] # Return empty dataframe with same index
+ except Exception as e:
+ print(f"Warning: Could not filter fundamentals by date: {e}")
+ return data
+
+
def get_balance_sheet(
ticker: Annotated[str, "ticker symbol of the company"],
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
- curr_date: Annotated[str, "current date (not used for yfinance)"] = None
+ curr_date: Annotated[str, "current date for point-in-time filtering"] = None
):
- """Get balance sheet data from yfinance."""
+ """Get balance sheet data from yfinance, filtered by curr_date for backtesting accuracy."""
try:
# Normalize symbol for yfinance (adds .NS suffix for NSE stocks)
normalized_ticker = normalize_symbol(ticker, target="yfinance")
ticker_obj = yf.Ticker(normalized_ticker)
-
+
if freq.lower() == "quarterly":
data = ticker_obj.quarterly_balance_sheet
else:
data = ticker_obj.balance_sheet
-
+
if data.empty:
return f"No balance sheet data found for symbol '{normalized_ticker}'"
+ # Filter by curr_date for point-in-time accuracy in backtesting
+ data = _filter_fundamentals_by_date(data, curr_date)
+
+ if data.empty:
+ return f"No balance sheet data available for {normalized_ticker} as of {curr_date}"
+
# Convert to CSV string for consistency with other functions
csv_string = data.to_csv()
# Add header information
header = f"# Balance Sheet data for {normalized_ticker} ({freq})\n"
+ if curr_date:
+ header += f"# Point-in-time data as of: {curr_date}\n"
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
return header + csv_string
@@ -332,9 +383,9 @@ def get_balance_sheet(
def get_cashflow(
ticker: Annotated[str, "ticker symbol of the company"],
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
- curr_date: Annotated[str, "current date (not used for yfinance)"] = None
+ curr_date: Annotated[str, "current date for point-in-time filtering"] = None
):
- """Get cash flow data from yfinance."""
+ """Get cash flow data from yfinance, filtered by curr_date for backtesting accuracy."""
try:
# Normalize symbol for yfinance (adds .NS suffix for NSE stocks)
normalized_ticker = normalize_symbol(ticker, target="yfinance")
@@ -348,11 +399,19 @@ def get_cashflow(
if data.empty:
return f"No cash flow data found for symbol '{normalized_ticker}'"
+ # Filter by curr_date for point-in-time accuracy in backtesting
+ data = _filter_fundamentals_by_date(data, curr_date)
+
+ if data.empty:
+ return f"No cash flow data available for {normalized_ticker} as of {curr_date}"
+
# Convert to CSV string for consistency with other functions
csv_string = data.to_csv()
# Add header information
header = f"# Cash Flow data for {normalized_ticker} ({freq})\n"
+ if curr_date:
+ header += f"# Point-in-time data as of: {curr_date}\n"
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
return header + csv_string
@@ -364,9 +423,9 @@ def get_cashflow(
def get_income_statement(
ticker: Annotated[str, "ticker symbol of the company"],
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
- curr_date: Annotated[str, "current date (not used for yfinance)"] = None
+ curr_date: Annotated[str, "current date for point-in-time filtering"] = None
):
- """Get income statement data from yfinance."""
+ """Get income statement data from yfinance, filtered by curr_date for backtesting accuracy."""
try:
# Normalize symbol for yfinance (adds .NS suffix for NSE stocks)
normalized_ticker = normalize_symbol(ticker, target="yfinance")
@@ -380,11 +439,19 @@ def get_income_statement(
if data.empty:
return f"No income statement data found for symbol '{normalized_ticker}'"
+ # Filter by curr_date for point-in-time accuracy in backtesting
+ data = _filter_fundamentals_by_date(data, curr_date)
+
+ if data.empty:
+ return f"No income statement data available for {normalized_ticker} as of {curr_date}"
+
# Convert to CSV string for consistency with other functions
csv_string = data.to_csv()
# Add header information
header = f"# Income Statement data for {normalized_ticker} ({freq})\n"
+ if curr_date:
+ header += f"# Point-in-time data as of: {curr_date}\n"
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
return header + csv_string
diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py
index f1866d93..2cdab4f6 100644
--- a/tradingagents/graph/trading_graph.py
+++ b/tradingagents/graph/trading_graph.py
@@ -1,11 +1,17 @@
# TradingAgents/graph/trading_graph.py
import os
+import sys
from pathlib import Path
import json
-from datetime import date
+from datetime import date, datetime
from typing import Dict, Any, Tuple, List, Optional
+# Add frontend backend to path for database access
+FRONTEND_BACKEND_PATH = Path(__file__).parent.parent.parent / "frontend" / "backend"
+if str(FRONTEND_BACKEND_PATH) not in sys.path:
+ sys.path.insert(0, str(FRONTEND_BACKEND_PATH))
+
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_google_genai import ChatGoogleGenerativeAI
@@ -191,6 +197,9 @@ class TradingAgentsGraph:
# Log state
self._log_state(trade_date, final_state)
+ # Save to frontend database for UI display
+ self._save_to_frontend_db(trade_date, final_state)
+
# Return decision and processed signal
return final_state, self.process_signal(final_state["final_trade_decision"])
@@ -236,6 +245,93 @@ class TradingAgentsGraph:
) as f:
json.dump(self.log_states_dict, f, indent=4)
+ def _save_to_frontend_db(self, trade_date: str, final_state: Dict[str, Any]):
+ """Save pipeline data to the frontend database for UI display.
+
+ Args:
+ trade_date: The date of the analysis
+ final_state: The final state from the graph execution
+ """
+ try:
+ from database import (
+ init_db,
+ save_agent_report,
+ save_debate_history,
+ save_pipeline_steps_bulk,
+ save_data_source_logs_bulk
+ )
+
+ # Initialize database if needed
+ init_db()
+
+ symbol = final_state.get("company_of_interest", self.ticker)
+ now = datetime.now().isoformat()
+
+ # 1. Save agent reports
+ agent_reports = [
+ ("market", final_state.get("market_report", "")),
+ ("news", final_state.get("news_report", "")),
+ ("social_media", final_state.get("sentiment_report", "")),
+ ("fundamentals", final_state.get("fundamentals_report", "")),
+ ]
+
+ for agent_type, content in agent_reports:
+ if content:
+ save_agent_report(
+ date=trade_date,
+ symbol=symbol,
+ agent_type=agent_type,
+ report_content=content,
+ data_sources_used=[]
+ )
+
+ # 2. Save investment debate
+ invest_debate = final_state.get("investment_debate_state", {})
+ if invest_debate:
+ save_debate_history(
+ date=trade_date,
+ symbol=symbol,
+ debate_type="investment",
+ bull_arguments=invest_debate.get("bull_history", ""),
+ bear_arguments=invest_debate.get("bear_history", ""),
+ judge_decision=invest_debate.get("judge_decision", ""),
+ full_history=invest_debate.get("history", "")
+ )
+
+ # 3. Save risk debate
+ risk_debate = final_state.get("risk_debate_state", {})
+ if risk_debate:
+ save_debate_history(
+ date=trade_date,
+ symbol=symbol,
+ debate_type="risk",
+ risky_arguments=risk_debate.get("risky_history", ""),
+ safe_arguments=risk_debate.get("safe_history", ""),
+ neutral_arguments=risk_debate.get("neutral_history", ""),
+ judge_decision=risk_debate.get("judge_decision", ""),
+ full_history=risk_debate.get("history", "")
+ )
+
+ # 4. Save pipeline steps (tracking the stages)
+ pipeline_steps = [
+ {"step_number": 1, "step_name": "initialize", "status": "completed", "started_at": now, "completed_at": now, "output_summary": "Pipeline initialized"},
+ {"step_number": 2, "step_name": "market_analysis", "status": "completed", "started_at": now, "completed_at": now, "output_summary": "Market analysis complete" if final_state.get("market_report") else "Skipped"},
+ {"step_number": 3, "step_name": "news_analysis", "status": "completed", "started_at": now, "completed_at": now, "output_summary": "News analysis complete" if final_state.get("news_report") else "Skipped"},
+ {"step_number": 4, "step_name": "social_analysis", "status": "completed", "started_at": now, "completed_at": now, "output_summary": "Social analysis complete" if final_state.get("sentiment_report") else "Skipped"},
+ {"step_number": 5, "step_name": "fundamental_analysis", "status": "completed", "started_at": now, "completed_at": now, "output_summary": "Fundamental analysis complete" if final_state.get("fundamentals_report") else "Skipped"},
+ {"step_number": 6, "step_name": "investment_debate", "status": "completed", "started_at": now, "completed_at": now, "output_summary": invest_debate.get("judge_decision", "")[:100] if invest_debate else "Skipped"},
+ {"step_number": 7, "step_name": "trader_decision", "status": "completed", "started_at": now, "completed_at": now, "output_summary": final_state.get("trader_investment_plan", "")[:100] if final_state.get("trader_investment_plan") else "Skipped"},
+ {"step_number": 8, "step_name": "risk_debate", "status": "completed", "started_at": now, "completed_at": now, "output_summary": risk_debate.get("judge_decision", "")[:100] if risk_debate else "Skipped"},
+ {"step_number": 9, "step_name": "final_decision", "status": "completed", "started_at": now, "completed_at": now, "output_summary": final_state.get("final_trade_decision", "")[:100] if final_state.get("final_trade_decision") else "Pending"},
+ ]
+ save_pipeline_steps_bulk(trade_date, symbol, pipeline_steps)
+
+ print(f"[Frontend DB] Saved pipeline data for {symbol} on {trade_date}")
+
+ except Exception as e:
+ print(f"[Frontend DB] Warning: Could not save to frontend database: {e}")
+ # Don't fail the main process if frontend DB save fails
+
def reflect_and_remember(self, returns_losses):
"""Reflect on decisions and update memory based on returns."""
self.reflector.reflect_bull_researcher(