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 66b912b..2968025 100644 --- a/src/agents.py +++ b/src/agents.py @@ -1,18 +1,33 @@ +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 +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 llm = ChatOpenAI(model="gpt-4o") @@ -42,47 +57,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 +123,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 +147,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 +155,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 +164,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 +173,7 @@ def quant_agent(state: AgentState): signals.append('bearish') else: signals.append('neutral') - + # Add reasoning collection reasoning = { "MACD": { @@ -169,22 +193,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 +230,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 +248,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 +257,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 +272,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 +287,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 +306,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 +331,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, @@ -354,7 +378,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") @@ -404,9 +431,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) @@ -463,7 +498,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 @@ -556,10 +591,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( @@ -579,21 +622,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 +678,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, @@ -675,25 +718,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) @@ -726,28 +774,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,