Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor FinancialDatasetAPI integration and the memory cache #42

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 99 additions & 21 deletions src/agents/fundamentals.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import json

from langchain_core.messages import HumanMessage

from graph.state import AgentState, show_agent_reasoning
from tools.api import FinancialDatasetAPI
from utils.progress import progress
import json

from tools.api import get_financial_metrics


##### Fundamental Agent #####
Expand All @@ -17,18 +18,22 @@ def fundamentals_agent(state: AgentState):
fundamental_analysis = {}

for ticker in tickers:
progress.update_status("fundamentals_agent", ticker, "Fetching financial metrics")
progress.update_status(
"fundamentals_agent", ticker, "Fetching financial metrics"
)

# Get the financial metrics
financial_metrics = get_financial_metrics(
financial_metrics = FinancialDatasetAPI().get_financial_metrics(
ticker=ticker,
end_date=end_date,
period="ttm",
limit=10,
)

if not financial_metrics:
progress.update_status("fundamentals_agent", ticker, "Failed: No financial metrics found")
progress.update_status(
"fundamentals_agent", ticker, "Failed: No financial metrics found"
)
continue

# Pull the most recent financial metrics
Expand All @@ -49,12 +54,31 @@ def fundamentals_agent(state: AgentState):
(net_margin, 0.20), # Healthy profit margins
(operating_margin, 0.15), # Strong operating efficiency
]
profitability_score = sum(metric is not None and metric > threshold for metric, threshold in thresholds)
profitability_score = sum(
metric is not None and metric > threshold
for metric, threshold in thresholds
)

signals.append("bullish" if profitability_score >= 2 else "bearish" if profitability_score == 0 else "neutral")
signals.append(
"bullish"
if profitability_score >= 2
else "bearish"
if profitability_score == 0
else "neutral"
)
reasoning["profitability_signal"] = {
"signal": signals[0],
"details": (f"ROE: {return_on_equity:.2%}" if return_on_equity else "ROE: N/A") + ", " + (f"Net Margin: {net_margin:.2%}" if net_margin else "Net Margin: N/A") + ", " + (f"Op Margin: {operating_margin:.2%}" if operating_margin else "Op Margin: N/A"),
"details": (
f"ROE: {return_on_equity:.2%}" if return_on_equity else "ROE: N/A"
)
+ ", "
+ (f"Net Margin: {net_margin:.2%}" if net_margin else "Net Margin: N/A")
+ ", "
+ (
f"Op Margin: {operating_margin:.2%}"
if operating_margin
else "Op Margin: N/A"
),
}

progress.update_status("fundamentals_agent", ticker, "Analyzing growth")
Expand All @@ -68,15 +92,36 @@ def fundamentals_agent(state: AgentState):
(earnings_growth, 0.10), # 10% earnings growth
(book_value_growth, 0.10), # 10% book value growth
]
growth_score = sum(metric is not None and metric > threshold for metric, threshold in thresholds)
growth_score = sum(
metric is not None and metric > threshold
for metric, threshold in thresholds
)

signals.append("bullish" if growth_score >= 2 else "bearish" if growth_score == 0 else "neutral")
signals.append(
"bullish"
if growth_score >= 2
else "bearish"
if growth_score == 0
else "neutral"
)
reasoning["growth_signal"] = {
"signal": signals[1],
"details": (f"Revenue Growth: {revenue_growth:.2%}" if revenue_growth else "Revenue Growth: N/A") + ", " + (f"Earnings Growth: {earnings_growth:.2%}" if earnings_growth else "Earnings Growth: N/A"),
"details": (
f"Revenue Growth: {revenue_growth:.2%}"
if revenue_growth
else "Revenue Growth: N/A"
)
+ ", "
+ (
f"Earnings Growth: {earnings_growth:.2%}"
if earnings_growth
else "Earnings Growth: N/A"
),
}

progress.update_status("fundamentals_agent", ticker, "Analyzing financial health")
progress.update_status(
"fundamentals_agent", ticker, "Analyzing financial health"
)
# 3. Financial Health
current_ratio = metrics.current_ratio
debt_to_equity = metrics.debt_to_equity
Expand All @@ -88,16 +133,34 @@ def fundamentals_agent(state: AgentState):
health_score += 1
if debt_to_equity and debt_to_equity < 0.5: # Conservative debt levels
health_score += 1
if free_cash_flow_per_share and earnings_per_share and free_cash_flow_per_share > earnings_per_share * 0.8: # Strong FCF conversion
if (
free_cash_flow_per_share
and earnings_per_share
and free_cash_flow_per_share > 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")
signals.append(
"bullish"
if health_score >= 2
else "bearish"
if health_score == 0
else "neutral"
)
reasoning["financial_health_signal"] = {
"signal": signals[2],
"details": (f"Current Ratio: {current_ratio:.2f}" if current_ratio else "Current Ratio: N/A") + ", " + (f"D/E: {debt_to_equity:.2f}" if debt_to_equity else "D/E: N/A"),
"details": (
f"Current Ratio: {current_ratio:.2f}"
if current_ratio
else "Current Ratio: N/A"
)
+ ", "
+ (f"D/E: {debt_to_equity:.2f}" if debt_to_equity else "D/E: N/A"),
}

progress.update_status("fundamentals_agent", ticker, "Analyzing valuation ratios")
progress.update_status(
"fundamentals_agent", ticker, "Analyzing valuation ratios"
)
# 4. Price to X ratios
pe_ratio = metrics.price_to_earnings_ratio
pb_ratio = metrics.price_to_book_ratio
Expand All @@ -108,12 +171,25 @@ def fundamentals_agent(state: AgentState):
(pb_ratio, 3), # Reasonable P/B ratio
(ps_ratio, 5), # Reasonable P/S ratio
]
price_ratio_score = sum(metric is not None and metric > threshold for metric, threshold in thresholds)
price_ratio_score = sum(
metric is not None and metric > threshold
for metric, threshold in thresholds
)

signals.append("bullish" if price_ratio_score >= 2 else "bearish" if price_ratio_score == 0 else "neutral")
signals.append(
"bullish"
if price_ratio_score >= 2
else "bearish"
if price_ratio_score == 0
else "neutral"
)
reasoning["price_ratios_signal"] = {
"signal": signals[3],
"details": (f"P/E: {pe_ratio:.2f}" if pe_ratio else "P/E: N/A") + ", " + (f"P/B: {pb_ratio:.2f}" if pb_ratio else "P/B: N/A") + ", " + (f"P/S: {ps_ratio:.2f}" if ps_ratio else "P/S: N/A"),
"details": (f"P/E: {pe_ratio:.2f}" if pe_ratio else "P/E: N/A")
+ ", "
+ (f"P/B: {pb_ratio:.2f}" if pb_ratio else "P/B: N/A")
+ ", "
+ (f"P/S: {ps_ratio:.2f}" if ps_ratio else "P/S: N/A"),
}

progress.update_status("fundamentals_agent", ticker, "Calculating final signal")
Expand All @@ -130,7 +206,9 @@ def fundamentals_agent(state: AgentState):

# Calculate confidence level
total_signals = len(signals)
confidence = round(max(bullish_signals, bearish_signals) / total_signals, 2) * 100
confidence = (
round(max(bullish_signals, bearish_signals) / total_signals, 2) * 100
)

fundamental_analysis[ticker] = {
"signal": overall_signal,
Expand Down
2 changes: 1 addition & 1 deletion src/agents/portfolio_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def generate_trading_decision(
- For sells: quantity must be ≤ current position shares
- For buys: quantity must be ≤ max_shares provided for each ticker
- The max_shares values are pre-calculated to respect position limits

Inputs:
- signals_by_ticker: dictionary of ticker to signals from analysts
- max_shares: maximum number of shares allowed for each ticker
Expand Down
53 changes: 34 additions & 19 deletions src/agents/sentiment.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import json

import numpy as np
import pandas as pd
from langchain_core.messages import HumanMessage

from graph.state import AgentState, show_agent_reasoning
from tools.api import FinancialDatasetAPI
from utils.progress import progress
import pandas as pd
import numpy as np
import json

from tools.api import get_insider_trades, get_company_news


##### Sentiment Agent #####
Expand All @@ -15,14 +16,16 @@ def sentiment_agent(state: AgentState):
end_date = data.get("end_date")
tickers = data.get("tickers")

financial_api = FinancialDatasetAPI()

# Initialize sentiment analysis for each ticker
sentiment_analysis = {}

for ticker in tickers:
progress.update_status("sentiment_agent", ticker, "Fetching insider trades")

# Get the insider trades
insider_trades = get_insider_trades(
insider_trades = financial_api.get_insider_trades(
ticker=ticker,
end_date=end_date,
limit=1000,
Expand All @@ -31,32 +34,39 @@ def sentiment_agent(state: AgentState):
progress.update_status("sentiment_agent", ticker, "Analyzing trading patterns")

# Get the signals from the insider trades
transaction_shares = pd.Series([t.transaction_shares for t in insider_trades]).dropna()
insider_signals = np.where(transaction_shares < 0, "bearish", "bullish").tolist()
transaction_shares = pd.Series(
[t.transaction_shares for t in insider_trades]
).dropna()
insider_signals = np.where(
transaction_shares < 0, "bearish", "bullish"
).tolist()

progress.update_status("sentiment_agent", ticker, "Fetching company news")

# Get the company news
company_news = get_company_news(ticker, end_date, limit=100)
company_news = financial_api.get_company_news(ticker, end_date, limit=100)

# Get the sentiment from the company news
sentiment = pd.Series([n.sentiment for n in company_news]).dropna()
news_signals = np.where(sentiment == "negative", "bearish",
np.where(sentiment == "positive", "bullish", "neutral")).tolist()

news_signals = np.where(
sentiment == "negative",
"bearish",
np.where(sentiment == "positive", "bullish", "neutral"),
).tolist()

progress.update_status("sentiment_agent", ticker, "Combining signals")
# Combine signals from both sources with weights
insider_weight = 0.3
news_weight = 0.7

# Calculate weighted signal counts
bullish_signals = (
insider_signals.count("bullish") * insider_weight +
news_signals.count("bullish") * news_weight
insider_signals.count("bullish") * insider_weight
+ news_signals.count("bullish") * news_weight
)
bearish_signals = (
insider_signals.count("bearish") * insider_weight +
news_signals.count("bearish") * news_weight
insider_signals.count("bearish") * insider_weight
+ news_signals.count("bearish") * news_weight
)

if bullish_signals > bearish_signals:
Expand All @@ -67,10 +77,15 @@ def sentiment_agent(state: AgentState):
overall_signal = "neutral"

# Calculate confidence level based on the weighted proportion
total_weighted_signals = len(insider_signals) * insider_weight + len(news_signals) * news_weight
total_weighted_signals = (
len(insider_signals) * insider_weight + len(news_signals) * news_weight
)
confidence = 0 # Default confidence when there are no signals
if total_weighted_signals > 0:
confidence = round(max(bullish_signals, bearish_signals) / total_weighted_signals, 2) * 100
confidence = (
round(max(bullish_signals, bearish_signals) / total_weighted_signals, 2)
* 100
)
reasoning = f"Weighted Bullish signals: {bullish_signals:.1f}, Weighted Bearish signals: {bearish_signals:.1f}"

sentiment_analysis[ticker] = {
Expand Down
Loading