From 3f4665fa2910132f7fc7771741c5f0ffd3c1d605 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Mon, 23 Dec 2024 10:42:34 -0500 Subject: [PATCH 1/5] Add error handling for data fetching in market_data_agent --- src/agents.py | 165 ++++++++++++++++++++++++++------------------------ 1 file changed, 87 insertions(+), 78 deletions(-) diff --git a/src/agents.py b/src/agents.py index 66b912b..52b2eee 100644 --- a/src/agents.py +++ b/src/agents.py @@ -42,47 +42,56 @@ def market_data_agent(state: AgentState): else: start_date = data["start_date"] - # Get the historical price data - prices = get_prices( - ticker=data["ticker"], - start_date=start_date, - end_date=end_date, - ) - - # Get the financial metrics - financial_metrics = get_financial_metrics( - ticker=data["ticker"], - report_period=end_date, - period='ttm', - limit=1, - ) - - # Get the insider trades - insider_trades = get_insider_trades( - ticker=data["ticker"], - end_date=end_date, - limit=5, - ) - - # Get the market cap - market_cap = get_market_cap( - ticker=data["ticker"], - ) + try: + # Get the historical price data + prices = get_prices( + ticker=data["ticker"], + start_date=start_date, + end_date=end_date, + ) + + # Get the financial metrics + financial_metrics = get_financial_metrics( + ticker=data["ticker"], + report_period=end_date, + period='ttm', + limit=1, + ) + + # Get the insider trades + insider_trades = get_insider_trades( + ticker=data["ticker"], + end_date=end_date, + limit=5, + ) + + # Get the market cap + market_cap = get_market_cap( + ticker=data["ticker"], + ) + + # Get the line_items + financial_line_items = search_line_items( + ticker=data["ticker"], + line_items=["free_cash_flow"], + period='ttm', + limit=1, + ) - # Get the line_items - financial_line_items = search_line_items( - ticker=data["ticker"], - line_items=["free_cash_flow"], - period='ttm', - limit=1, - ) + except Exception as e: + error_message = f"Error fetching data: {str(e)}" + print(error_message) + return { + "messages": messages, + "data": {**data, "error": error_message} + } return { "messages": messages, "data": { - **data, - "prices": prices, - "start_date": start_date, + **data, + "prices": prices, + "start_date": start_date, "end_date": end_date, "financial_metrics": financial_metrics, "insider_trades": insider_trades, @@ -99,23 +108,23 @@ def quant_agent(state: AgentState): data = state["data"] prices = data["prices"] prices_df = prices_to_df(prices) - + # Calculate indicators # 1. MACD (Moving Average Convergence Divergence) macd_line, signal_line = calculate_macd(prices_df) - + # 2. RSI (Relative Strength Index) rsi = calculate_rsi(prices_df) - + # 3. Bollinger Bands (Bollinger Bands) upper_band, lower_band = calculate_bollinger_bands(prices_df) - + # 4. OBV (On-Balance Volume) obv = calculate_obv(prices_df) - + # Generate individual signals signals = [] - + # MACD signal if macd_line.iloc[-2] < signal_line.iloc[-2] and macd_line.iloc[-1] > signal_line.iloc[-1]: signals.append('bullish') @@ -123,7 +132,7 @@ def quant_agent(state: AgentState): signals.append('bearish') else: signals.append('neutral') - + # RSI signal if rsi.iloc[-1] < 30: signals.append('bullish') @@ -131,7 +140,7 @@ def quant_agent(state: AgentState): signals.append('bearish') else: signals.append('neutral') - + # Bollinger Bands signal current_price = prices_df['close'].iloc[-1] if current_price < lower_band.iloc[-1]: @@ -140,7 +149,7 @@ def quant_agent(state: AgentState): signals.append('bearish') else: signals.append('neutral') - + # OBV signal obv_slope = obv.diff().iloc[-5:].mean() if obv_slope > 0: @@ -149,7 +158,7 @@ def quant_agent(state: AgentState): signals.append('bearish') else: signals.append('neutral') - + # Add reasoning collection reasoning = { "MACD": { @@ -169,22 +178,22 @@ def quant_agent(state: AgentState): "details": f"OBV slope is {obv_slope:.2f} ({signals[3]})" } } - + # Determine overall signal bullish_signals = signals.count('bullish') bearish_signals = signals.count('bearish') - + if bullish_signals > bearish_signals: overall_signal = 'bullish' elif bearish_signals > bullish_signals: overall_signal = 'bearish' else: overall_signal = 'neutral' - + # Calculate confidence level based on the proportion of indicators agreeing total_signals = len(signals) confidence = max(bullish_signals, bearish_signals) / total_signals - + # Generate the message content message_content = { "signal": overall_signal, @@ -206,7 +215,7 @@ def quant_agent(state: AgentState): # Print the reasoning if the flag is set if show_reasoning: show_agent_reasoning(message_content, "Quant Agent") - + return { "messages": [message], "data": data, @@ -224,7 +233,7 @@ def fundamentals_agent(state: AgentState): # Initialize signals list for different fundamental aspects signals = [] reasoning = {} - + # 1. Profitability Analysis profitability_score = 0 if metrics["return_on_equity"] > 0.15: # Strong ROE above 15% @@ -233,13 +242,13 @@ def fundamentals_agent(state: AgentState): profitability_score += 1 if metrics["operating_margin"] > 0.15: # Strong operating efficiency profitability_score += 1 - + signals.append('bullish' if profitability_score >= 2 else 'bearish' if profitability_score == 0 else 'neutral') reasoning["Profitability"] = { "signal": signals[0], "details": f"ROE: {metrics['return_on_equity']:.2%}, Net Margin: {metrics['net_margin']:.2%}, Op Margin: {metrics['operating_margin']:.2%}" } - + # 2. Growth Analysis growth_score = 0 if metrics["revenue_growth"] > 0.10: # 10% revenue growth @@ -248,13 +257,13 @@ def fundamentals_agent(state: AgentState): growth_score += 1 if metrics["book_value_growth"] > 0.10: # 10% book value growth growth_score += 1 - + signals.append('bullish' if growth_score >= 2 else 'bearish' if growth_score == 0 else 'neutral') reasoning["Growth"] = { "signal": signals[1], "details": f"Revenue Growth: {metrics['revenue_growth']:.2%}, Earnings Growth: {metrics['earnings_growth']:.2%}" } - + # 3. Financial Health health_score = 0 if metrics["current_ratio"] > 1.5: # Strong liquidity @@ -263,18 +272,18 @@ def fundamentals_agent(state: AgentState): health_score += 1 if metrics["free_cash_flow_per_share"] > metrics["earnings_per_share"] * 0.8: # Strong FCF conversion health_score += 1 - + signals.append('bullish' if health_score >= 2 else 'bearish' if health_score == 0 else 'neutral') reasoning["Financial_Health"] = { "signal": signals[2], "details": f"Current Ratio: {metrics['current_ratio']:.2f}, D/E: {metrics['debt_to_equity']:.2f}" } - + # 4. Price to X ratios pe_ratio = metrics["price_to_earnings_ratio"] pb_ratio = metrics["price_to_book_ratio"] ps_ratio = metrics["price_to_sales_ratio"] - + price_ratio_score = 0 if pe_ratio < 25: # Reasonable P/E ratio price_ratio_score += 1 @@ -282,7 +291,7 @@ def fundamentals_agent(state: AgentState): price_ratio_score += 1 if ps_ratio < 5: # Reasonable P/S ratio price_ratio_score += 1 - + signals.append('bullish' if price_ratio_score >= 2 else 'bearish' if price_ratio_score == 0 else 'neutral') reasoning["Price_Ratios"] = { "signal": signals[3], @@ -307,38 +316,38 @@ def fundamentals_agent(state: AgentState): "signal": signals[4], "details": f"Intrinsic Value: ${intrinsic_value:,.2f}, Market Cap: ${market_cap:,.2f}" } - + # Determine overall signal bullish_signals = signals.count('bullish') bearish_signals = signals.count('bearish') - + if bullish_signals > bearish_signals: overall_signal = 'bullish' elif bearish_signals > bullish_signals: overall_signal = 'bearish' else: overall_signal = 'neutral' - + # Calculate confidence level total_signals = len(signals) confidence = max(bullish_signals, bearish_signals) / total_signals - + message_content = { "signal": overall_signal, "confidence": f"{round(confidence * 100)}%", "reasoning": reasoning } - + # Create the fundamental analysis message message = HumanMessage( content=str(message_content), name="fundamentals_agent", ) - + # Print the reasoning if the flag is set if show_reasoning: show_agent_reasoning(message_content, "Fundamental Analysis Agent") - + return { "messages": [message], "data": data, @@ -463,7 +472,7 @@ def risk_management_agent(state: AgentState): total_portfolio_value = portfolio['cash'] + current_stock_value base_position_size = total_portfolio_value * 0.25 # Start with 25% max position of total portfolio - + if market_risk_score >= 4: # Reduce position for high risk max_position_size = base_position_size * 0.5 @@ -579,21 +588,21 @@ def portfolio_management_agent(state: AgentState): 1. Fundamental Analysis (50% weight) - Primary driver of trading decisions - Should determine overall direction - + 2. Technical/Quant Analysis (35% weight) - Secondary confirmation - Helps with entry/exit timing - + 3. Sentiment Analysis (15% weight) - Final consideration - Can influence sizing within risk limits - + The decision process should be: 1. First check risk management constraints 2. Then evaluate fundamental outlook 3. Use technical analysis for timing 4. Consider sentiment for final adjustment - + Provide the following in your output: - "action": "buy" | "sell" | "hold", - "quantity": @@ -635,7 +644,7 @@ def portfolio_management_agent(state: AgentState): # Generate the prompt prompt = template.invoke( { - "quant_message": quant_message.content, + "quant_message": quant_message.content, "fundamentals_message": fundamentals_message.content, "sentiment_message": sentiment_message.content, "risk_message": risk_message.content, @@ -726,28 +735,28 @@ def run_hedge_fund(ticker: str, start_date: str, end_date: str, portfolio: dict, parser.add_argument('--start-date', type=str, help='Start date (YYYY-MM-DD). Defaults to 3 months before end date') parser.add_argument('--end-date', type=str, help='End date (YYYY-MM-DD). Defaults to today') parser.add_argument('--show-reasoning', action='store_true', help='Show reasoning from each agent') - + args = parser.parse_args() - + # Validate dates if provided if args.start_date: try: datetime.strptime(args.start_date, '%Y-%m-%d') except ValueError: raise ValueError("Start date must be in YYYY-MM-DD format") - + if args.end_date: try: datetime.strptime(args.end_date, '%Y-%m-%d') except ValueError: raise ValueError("End date must be in YYYY-MM-DD format") - + # Sample portfolio - you might want to make this configurable too portfolio = { "cash": 100000.0, # $100,000 initial cash "stock": 0 # No initial stock position } - + result = run_hedge_fund( ticker=args.ticker, start_date=args.start_date, From 3e68b1414a34721d8a26f98d3b22829d87e78c9e Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Mon, 23 Dec 2024 10:44:29 -0500 Subject: [PATCH 2/5] Add validation for transaction shares in sentiment agent --- src/agents.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/agents.py b/src/agents.py index 52b2eee..3a9e289 100644 --- a/src/agents.py +++ b/src/agents.py @@ -363,7 +363,10 @@ def sentiment_agent(state: AgentState): # Loop through the insider trades, if transaction_shares is negative, then it is a sell, which is bearish, if positive, then it is a buy, which is bullish signals = [] for trade in insider_trades: - if trade["transaction_shares"] < 0: + transaction_shares = trade.get("transaction_shares") + if transaction_shares is None: + continue + if transaction_shares < 0: signals.append("bearish") else: signals.append("bullish") From 00f8c74237084cc28541b45d4810db7bb7086320 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Mon, 23 Dec 2024 10:47:50 -0500 Subject: [PATCH 3/5] Add error handling for message fetching and hedge fund execution in agents --- src/agents.py | 71 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/src/agents.py b/src/agents.py index 3a9e289..83ecfca 100644 --- a/src/agents.py +++ b/src/agents.py @@ -416,9 +416,17 @@ def risk_management_agent(state: AgentState): prices_df = prices_to_df(data["prices"]) # Fetch messages from other agents - quant_message = next(msg for msg in state["messages"] if msg.name == "quant_agent") - fundamentals_message = next(msg for msg in state["messages"] if msg.name == "fundamentals_agent") - sentiment_message = next(msg for msg in state["messages"] if msg.name == "sentiment_agent") + try: + quant_message = next(msg for msg in state["messages"] if msg.name == "quant_agent") + fundamentals_message = next(msg for msg in state["messages"] if msg.name == "fundamentals_agent") + sentiment_message = next(msg for msg in state["messages"] if msg.name == "sentiment_agent") + except StopIteration as e: + error_message = f"Error fetching messages from other agents: {str(e)}" + print(error_message) + return { + "messages": state["messages"], + "data": {**data, "error": error_message} + } try: fundamental_signals = json.loads(fundamentals_message.content) @@ -568,10 +576,18 @@ def portfolio_management_agent(state: AgentState): portfolio = state["data"]["portfolio"] # Get the quant agent, fundamentals agent, and risk management agent messages - quant_message = next(msg for msg in state["messages"] if msg.name == "quant_agent") - fundamentals_message = next(msg for msg in state["messages"] if msg.name == "fundamentals_agent") - sentiment_message = next(msg for msg in state["messages"] if msg.name == "sentiment_agent") - risk_message = next(msg for msg in state["messages"] if msg.name == "risk_management_agent") + try: + quant_message = next(msg for msg in state["messages"] if msg.name == "quant_agent") + fundamentals_message = next(msg for msg in state["messages"] if msg.name == "fundamentals_agent") + sentiment_message = next(msg for msg in state["messages"] if msg.name == "sentiment_agent") + risk_message = next(msg for msg in state["messages"] if msg.name == "risk_management_agent") + except StopIteration as e: + error_message = f"Error fetching messages from other agents: {str(e)}" + print(error_message) + return { + "messages": state["messages"], + "data": {**state["data"], "error": error_message} + } # Create the prompt template template = ChatPromptTemplate.from_messages( @@ -687,25 +703,30 @@ def show_agent_reasoning(output, agent_name): ##### Run the Hedge Fund ##### def run_hedge_fund(ticker: str, start_date: str, end_date: str, portfolio: dict, show_reasoning: bool = False): - final_state = app.invoke( - { - "messages": [ - HumanMessage( - content="Make a trading decision based on the provided data.", - ) - ], - "data": { - "ticker": ticker, - "portfolio": portfolio, - "start_date": start_date, - "end_date": end_date, + try: + final_state = app.invoke( + { + "messages": [ + HumanMessage( + content="Make a trading decision based on the provided data.", + ) + ], + "data": { + "ticker": ticker, + "portfolio": portfolio, + "start_date": start_date, + "end_date": end_date, + }, + "metadata": { + "show_reasoning": show_reasoning, + } }, - "metadata": { - "show_reasoning": show_reasoning, - } - }, - ) - return final_state["messages"][-1].content + ) + return final_state["messages"][-1].content + except Exception as e: + error_message = f"Error running hedge fund: {str(e)}" + print(error_message) + return error_message # Define the new workflow workflow = StateGraph(AgentState) From fec45d1d36480c384119d5d722c034278499e403 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Mon, 23 Dec 2024 11:09:01 -0500 Subject: [PATCH 4/5] Load environment variables from .env file and update python-dotenv version constraint --- pyproject.toml | 2 +- src/agents.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e48f692..2294f09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ langchain-openai = "0.2.11" langgraph = "0.2.56" pandas = "^2.1.0" numpy = "^1.24.0" -python-dotenv = "1.0.0" +python-dotenv = "^1.0.0" matplotlib = "^3.9.2" [tool.poetry.group.dev.dependencies] diff --git a/src/agents.py b/src/agents.py index 83ecfca..0800bd1 100644 --- a/src/agents.py +++ b/src/agents.py @@ -13,6 +13,10 @@ from datetime import datetime import json import ast +import os +from dotenv import load_dotenv + +load_dotenv() # Load environment variables from .env file llm = ChatOpenAI(model="gpt-4o") From 7aa131820a6da1408eaccfd62ab6e280027fe739 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Mon, 23 Dec 2024 11:12:07 -0500 Subject: [PATCH 5/5] Refactor imports in agents.py for improved organization and readability --- src/agents.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/agents.py b/src/agents.py index 0800bd1..2968025 100644 --- a/src/agents.py +++ b/src/agents.py @@ -1,20 +1,31 @@ +import argparse +import ast +import json import math +import operator +import os +from datetime import datetime from typing import Annotated, Any, Dict, Sequence, TypedDict -import operator +from dotenv import load_dotenv from langchain_core.messages import BaseMessage, HumanMessage from langchain_core.prompts import ChatPromptTemplate from langchain_openai.chat_models import ChatOpenAI from langgraph.graph import END, StateGraph -from src.tools import calculate_bollinger_bands, calculate_intrinsic_value, calculate_macd, calculate_obv, calculate_rsi, search_line_items, get_financial_metrics, get_insider_trades, get_market_cap, get_prices, prices_to_df - -import argparse -from datetime import datetime -import json -import ast -import os -from dotenv import load_dotenv +from src.tools import ( + calculate_bollinger_bands, + calculate_intrinsic_value, + calculate_macd, + calculate_obv, + calculate_rsi, + get_financial_metrics, + get_insider_trades, + get_market_cap, + get_prices, + prices_to_df, + search_line_items, +) load_dotenv() # Load environment variables from .env file