Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions app/backend/models/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class BaseHedgeFundRequest(BaseModel):
agent_models: Optional[List[AgentModelConfig]] = None
model_name: Optional[str] = "gpt-4.1"
model_provider: Optional[ModelProvider] = ModelProvider.OPENAI
data_provider: Optional[str] = "yfinance" # Default to free option
margin_requirement: float = 0.0
portfolio_positions: Optional[List[PortfolioPosition]] = None
api_keys: Optional[Dict[str, str]] = None
Expand Down
6 changes: 4 additions & 2 deletions app/backend/services/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,12 @@ def create_graph(graph_nodes: list, graph_edges: list) -> StateGraph:
return graph


async def run_graph_async(graph, portfolio, tickers, start_date, end_date, model_name, model_provider, request=None):
async def run_graph_async(graph, portfolio, tickers, start_date, end_date, model_name, model_provider, data_provider="yfinance", request=None):
"""Async wrapper for run_graph to work with asyncio."""
# Use run_in_executor to run the synchronous function in a separate thread
# so it doesn't block the event loop
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, lambda: run_graph(graph, portfolio, tickers, start_date, end_date, model_name, model_provider, request)) # Use default executor
result = await loop.run_in_executor(None, lambda: run_graph(graph, portfolio, tickers, start_date, end_date, model_name, model_provider, data_provider, request)) # Use default executor
return result


Expand All @@ -146,6 +146,7 @@ def run_graph(
end_date: str,
model_name: str,
model_provider: str,
data_provider: str = "yfinance",
request=None,
) -> dict:
"""
Expand All @@ -171,6 +172,7 @@ def run_graph(
"show_reasoning": False,
"model_name": model_name,
"model_provider": model_provider,
"data_provider": data_provider,
"request": request, # Pass the request for agent-specific model access
},
},
Expand Down
376 changes: 353 additions & 23 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ httpx = "^0.27.0"
sqlalchemy = "^2.0.22"
alembic = "^1.12.0"
langchain-gigachat = "^0.3.12"
yfinance = "^0.2.65"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
Expand Down
8 changes: 6 additions & 2 deletions src/agents/aswath_damodaran.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
get_market_cap,
search_line_items,
)
from src.data.providers import get_data_provider_for_agent
from src.utils.api_key import get_api_key_from_state
from src.utils.llm import call_llm
from src.utils.progress import progress
Expand All @@ -37,14 +38,16 @@ def aswath_damodaran_agent(state: AgentState, agent_id: str = "aswath_damodaran_
end_date = data["end_date"]
tickers = data["tickers"]
api_key = get_api_key_from_state(state, "FINANCIAL_DATASETS_API_KEY")
# Use centralized data provider configuration
data_provider = get_data_provider_for_agent(state, agent_id)

analysis_data: dict[str, dict] = {}
damodaran_signals: dict[str, dict] = {}

for ticker in tickers:
# ─── Fetch core data ────────────────────────────────────────────────────
progress.update_status(agent_id, ticker, "Fetching financial metrics")
metrics = get_financial_metrics(ticker, end_date, period="ttm", limit=5, api_key=api_key)
metrics = get_financial_metrics(ticker, end_date, period="ttm", limit=5, api_key=api_key, data_provider=data_provider)

progress.update_status(agent_id, ticker, "Fetching financial line items")
line_items = search_line_items(
Expand All @@ -61,10 +64,11 @@ def aswath_damodaran_agent(state: AgentState, agent_id: str = "aswath_damodaran_
],
end_date,
api_key=api_key,
data_provider=data_provider,
)

progress.update_status(agent_id, ticker, "Getting market cap")
market_cap = get_market_cap(ticker, end_date, api_key=api_key)
market_cap = get_market_cap(ticker, end_date, api_key=api_key, data_provider=data_provider)

# ─── Analyses ───────────────────────────────────────────────────────────
progress.update_status(agent_id, ticker, "Analyzing growth and reinvestment")
Expand Down
42 changes: 26 additions & 16 deletions src/agents/ben_graham.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from src.graph.state import AgentState, show_agent_reasoning
from src.tools.api import get_financial_metrics, get_market_cap, search_line_items
from src.data.providers import get_data_provider_for_agent
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import HumanMessage
from pydantic import BaseModel
Expand Down Expand Up @@ -29,19 +30,21 @@ def ben_graham_agent(state: AgentState, agent_id: str = "ben_graham_agent"):
end_date = data["end_date"]
tickers = data["tickers"]
api_key = get_api_key_from_state(state, "FINANCIAL_DATASETS_API_KEY")
# Use centralized data provider configuration
data_provider = get_data_provider_for_agent(state, agent_id)

analysis_data = {}
graham_analysis = {}

for ticker in tickers:
progress.update_status(agent_id, ticker, "Fetching financial metrics")
metrics = get_financial_metrics(ticker, end_date, period="annual", limit=10, api_key=api_key)
metrics = get_financial_metrics(ticker, end_date, period="annual", limit=10, api_key=api_key, data_provider=data_provider)

progress.update_status(agent_id, ticker, "Gathering financial line items")
financial_line_items = search_line_items(ticker, ["earnings_per_share", "revenue", "net_income", "book_value_per_share", "total_assets", "total_liabilities", "current_assets", "current_liabilities", "dividends_and_other_cash_distributions", "outstanding_shares"], end_date, period="annual", limit=10, api_key=api_key)
financial_line_items = search_line_items(ticker, ["earnings_per_share", "revenue", "net_income", "book_value_per_share", "total_assets", "total_liabilities", "current_assets", "current_liabilities", "dividends_and_other_cash_distributions", "outstanding_shares"], end_date, period="annual", limit=10, api_key=api_key, data_provider=data_provider)

progress.update_status(agent_id, ticker, "Getting market cap")
market_cap = get_market_cap(ticker, end_date, api_key=api_key)
market_cap = get_market_cap(ticker, end_date, api_key=api_key, data_provider=data_provider)

# Perform sub-analyses
progress.update_status(agent_id, ticker, "Analyzing earnings stability")
Expand Down Expand Up @@ -109,7 +112,7 @@ def analyze_earnings_stability(metrics: list, financial_line_items: list) -> dic

eps_vals = []
for item in financial_line_items:
if item.earnings_per_share is not None:
if hasattr(item, "earnings_per_share") and item.earnings_per_share is not None:
eps_vals.append(item.earnings_per_share)

if len(eps_vals) < 2:
Expand Down Expand Up @@ -149,11 +152,10 @@ def analyze_financial_strength(financial_line_items: list) -> dict:
if not financial_line_items:
return {"score": score, "details": "No data for financial strength analysis"}

latest_item = financial_line_items[0]
total_assets = latest_item.total_assets or 0
total_liabilities = latest_item.total_liabilities or 0
current_assets = latest_item.current_assets or 0
current_liabilities = latest_item.current_liabilities or 0
total_assets = get_line_item_value(financial_line_items, "total_assets")
total_liabilities = get_line_item_value(financial_line_items, "total_liabilities")
current_assets = get_line_item_value(financial_line_items, "current_assets")
current_liabilities = get_line_item_value(financial_line_items, "current_liabilities")

# 1. Current ratio
if current_liabilities > 0:
Expand Down Expand Up @@ -184,7 +186,7 @@ def analyze_financial_strength(financial_line_items: list) -> dict:
details.append("Cannot compute debt ratio (missing total_assets).")

# 3. Dividend track record
div_periods = [item.dividends_and_other_cash_distributions for item in financial_line_items if item.dividends_and_other_cash_distributions is not None]
div_periods = [item.dividends_and_other_cash_distributions for item in financial_line_items if hasattr(item, "dividends_and_other_cash_distributions") and item.dividends_and_other_cash_distributions is not None]
if div_periods:
# In many data feeds, dividend outflow is shown as a negative number
# (money going out to shareholders). We'll consider any negative as 'paid a dividend'.
Expand All @@ -204,6 +206,15 @@ def analyze_financial_strength(financial_line_items: list) -> dict:
return {"score": score, "details": "; ".join(details)}


def get_line_item_value(financial_line_items: list, line_item_name: str) -> float:
"""Helper function to extract value for a specific line item."""
for item in financial_line_items:
if hasattr(item, line_item_name):
value = getattr(item, line_item_name)
if value is not None:
return value
return 0

def analyze_valuation_graham(financial_line_items: list, market_cap: float) -> dict:
"""
Core Graham approach to valuation:
Expand All @@ -214,12 +225,11 @@ def analyze_valuation_graham(financial_line_items: list, market_cap: float) -> d
if not financial_line_items or not market_cap or market_cap <= 0:
return {"score": 0, "details": "Insufficient data to perform valuation"}

latest = financial_line_items[0]
current_assets = latest.current_assets or 0
total_liabilities = latest.total_liabilities or 0
book_value_ps = latest.book_value_per_share or 0
eps = latest.earnings_per_share or 0
shares_outstanding = latest.outstanding_shares or 0
current_assets = get_line_item_value(financial_line_items, "current_assets")
total_liabilities = get_line_item_value(financial_line_items, "total_liabilities")
book_value_ps = get_line_item_value(financial_line_items, "book_value_per_share")
eps = get_line_item_value(financial_line_items, "earnings_per_share")
shares_outstanding = get_line_item_value(financial_line_items, "outstanding_shares")

details = []
score = 0
Expand Down
26 changes: 15 additions & 11 deletions src/agents/bill_ackman.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from src.graph.state import AgentState, show_agent_reasoning
from src.tools.api import get_financial_metrics, get_market_cap, search_line_items
from src.data.providers import get_data_provider_for_agent
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import HumanMessage
from pydantic import BaseModel
Expand All @@ -26,12 +27,14 @@ def bill_ackman_agent(state: AgentState, agent_id: str = "bill_ackman_agent"):
end_date = data["end_date"]
tickers = data["tickers"]
api_key = get_api_key_from_state(state, "FINANCIAL_DATASETS_API_KEY")
# Use centralized data provider configuration
data_provider = get_data_provider_for_agent(state, agent_id)
analysis_data = {}
ackman_analysis = {}

for ticker in tickers:
progress.update_status(agent_id, ticker, "Fetching financial metrics")
metrics = get_financial_metrics(ticker, end_date, period="annual", limit=5, api_key=api_key)
metrics = get_financial_metrics(ticker, end_date, period="annual", limit=5, api_key=api_key, data_provider=data_provider)

progress.update_status(agent_id, ticker, "Gathering financial line items")
# Request multiple periods of data (annual or TTM) for a more robust long-term view.
Expand All @@ -53,10 +56,11 @@ def bill_ackman_agent(state: AgentState, agent_id: str = "bill_ackman_agent"):
period="annual",
limit=5,
api_key=api_key,
data_provider=data_provider,
)

progress.update_status(agent_id, ticker, "Getting market cap")
market_cap = get_market_cap(ticker, end_date, api_key=api_key)
market_cap = get_market_cap(ticker, end_date, api_key=api_key, data_provider=data_provider)

progress.update_status(agent_id, ticker, "Analyzing business quality")
quality_analysis = analyze_business_quality(metrics, financial_line_items)
Expand Down Expand Up @@ -150,7 +154,7 @@ def analyze_business_quality(metrics: list, financial_line_items: list) -> dict:
}

# 1. Multi-period revenue growth analysis
revenues = [item.revenue for item in financial_line_items if item.revenue is not None]
revenues = [getattr(item, 'revenue', None) for item in financial_line_items if getattr(item, 'revenue', None) is not None]
if len(revenues) >= 2:
initial, final = revenues[-1], revenues[0]
if initial and final and final > initial:
Expand All @@ -167,8 +171,8 @@ def analyze_business_quality(metrics: list, financial_line_items: list) -> dict:
details.append("Not enough revenue data for multi-period trend.")

# 2. Operating margin and free cash flow consistency
fcf_vals = [item.free_cash_flow for item in financial_line_items if item.free_cash_flow is not None]
op_margin_vals = [item.operating_margin for item in financial_line_items if item.operating_margin is not None]
fcf_vals = [getattr(item, 'free_cash_flow', None) for item in financial_line_items if getattr(item, 'free_cash_flow', None) is not None]
op_margin_vals = [getattr(item, 'operating_margin', None) for item in financial_line_items if getattr(item, 'operating_margin', None) is not None]

if op_margin_vals:
above_15 = sum(1 for m in op_margin_vals if m > 0.15)
Expand Down Expand Up @@ -228,7 +232,7 @@ def analyze_financial_discipline(metrics: list, financial_line_items: list) -> d
}

# 1. Multi-period debt ratio or debt_to_equity
debt_to_equity_vals = [item.debt_to_equity for item in financial_line_items if item.debt_to_equity is not None]
debt_to_equity_vals = [getattr(item, 'debt_to_equity', None) for item in financial_line_items if getattr(item, 'debt_to_equity', None) is not None]
if debt_to_equity_vals:
below_one_count = sum(1 for d in debt_to_equity_vals if d < 1.0)
if below_one_count >= (len(debt_to_equity_vals) // 2 + 1):
Expand All @@ -255,9 +259,9 @@ def analyze_financial_discipline(metrics: list, financial_line_items: list) -> d

# 2. Capital allocation approach (dividends + share counts)
dividends_list = [
item.dividends_and_other_cash_distributions
getattr(item, 'dividends_and_other_cash_distributions', None)
for item in financial_line_items
if item.dividends_and_other_cash_distributions is not None
if getattr(item, 'dividends_and_other_cash_distributions', None) is not None
]
if dividends_list:
paying_dividends_count = sum(1 for d in dividends_list if d < 0)
Expand All @@ -270,7 +274,7 @@ def analyze_financial_discipline(metrics: list, financial_line_items: list) -> d
details.append("No dividend data found across periods.")

# Check for decreasing share count (simple approach)
shares = [item.outstanding_shares for item in financial_line_items if item.outstanding_shares is not None]
shares = [getattr(item, 'outstanding_shares', None) for item in financial_line_items if getattr(item, 'outstanding_shares', None) is not None]
if len(shares) >= 2:
# For buybacks, the newest count should be less than the oldest count
if shares[0] < shares[-1]:
Expand Down Expand Up @@ -303,8 +307,8 @@ def analyze_activism_potential(financial_line_items: list) -> dict:
}

# Check revenue growth vs. operating margin
revenues = [item.revenue for item in financial_line_items if item.revenue is not None]
op_margins = [item.operating_margin for item in financial_line_items if item.operating_margin is not None]
revenues = [getattr(item, 'revenue', None) for item in financial_line_items if getattr(item, 'revenue', None) is not None]
op_margins = [getattr(item, 'operating_margin', None) for item in financial_line_items if getattr(item, 'operating_margin', None) is not None]

if len(revenues) < 2 or not op_margins:
return {
Expand Down
Loading