Add per-agent skill workspaces and TraderView management
This commit is contained in:
@@ -22,6 +22,16 @@ from agentscope.message import TextBlock
|
||||
from agentscope.tool import ToolResponse
|
||||
|
||||
from backend.data.provider_utils import normalize_symbol
|
||||
from backend.skills.builtin.valuation_review.scripts.dcf_report import (
|
||||
build_dcf_report,
|
||||
)
|
||||
from backend.skills.builtin.valuation_review.scripts.multiple_valuation_report import (
|
||||
build_ev_ebitda_report,
|
||||
build_residual_income_report,
|
||||
)
|
||||
from backend.skills.builtin.valuation_review.scripts.owner_earnings_report import (
|
||||
build_owner_earnings_report,
|
||||
)
|
||||
from backend.tools.data_tools import (
|
||||
get_company_news,
|
||||
get_financial_metrics,
|
||||
@@ -814,7 +824,7 @@ def dcf_valuation_analysis(
|
||||
|
||||
current_date = _resolved_date(current_date)
|
||||
tickers = _parse_tickers(tickers)
|
||||
lines = [f"=== DCF Valuation Analysis ({current_date}) ===\n"]
|
||||
rows = []
|
||||
|
||||
for ticker in tickers:
|
||||
metrics = get_financial_metrics(
|
||||
@@ -823,7 +833,7 @@ def dcf_valuation_analysis(
|
||||
limit=8,
|
||||
)
|
||||
if not metrics:
|
||||
lines.append(f"{ticker}: No financial metrics\n")
|
||||
rows.append({"ticker": ticker, "error": "No financial metrics"})
|
||||
continue
|
||||
|
||||
line_items = search_line_items(
|
||||
@@ -838,56 +848,28 @@ def dcf_valuation_analysis(
|
||||
or not line_items[0].free_cash_flow
|
||||
or line_items[0].free_cash_flow <= 0
|
||||
):
|
||||
lines.append(f"{ticker}: Invalid free cash flow data\n")
|
||||
rows.append({"ticker": ticker, "error": "Invalid free cash flow data"})
|
||||
continue
|
||||
|
||||
market_cap = get_market_cap(ticker, current_date)
|
||||
if not market_cap:
|
||||
lines.append(f"{ticker}: Market cap unavailable\n")
|
||||
rows.append({"ticker": ticker, "error": "Market cap unavailable"})
|
||||
continue
|
||||
|
||||
m = metrics[0]
|
||||
current_fcf = line_items[0].free_cash_flow
|
||||
growth_rate = m.earnings_growth or 0.05
|
||||
discount_rate = 0.10
|
||||
terminal_growth = 0.03
|
||||
num_years = 5
|
||||
|
||||
# DCF calculation
|
||||
pv_fcf = sum(
|
||||
current_fcf
|
||||
* (1 + growth_rate) ** year
|
||||
/ (1 + discount_rate) ** year
|
||||
for year in range(1, num_years + 1)
|
||||
rows.append(
|
||||
{
|
||||
"ticker": ticker,
|
||||
"current_fcf": line_items[0].free_cash_flow,
|
||||
"growth_rate": m.earnings_growth or 0.05,
|
||||
"market_cap": market_cap,
|
||||
"discount_rate": 0.10,
|
||||
"terminal_growth": 0.03,
|
||||
"num_years": 5,
|
||||
},
|
||||
)
|
||||
terminal_fcf = (
|
||||
current_fcf
|
||||
* (1 + growth_rate) ** num_years
|
||||
* (1 + terminal_growth)
|
||||
)
|
||||
terminal_value = terminal_fcf / (discount_rate - terminal_growth)
|
||||
pv_terminal = terminal_value / (1 + discount_rate) ** num_years
|
||||
enterprise_value = pv_fcf + pv_terminal
|
||||
value_gap = (enterprise_value - market_cap) / market_cap * 100
|
||||
|
||||
# Assessment
|
||||
if value_gap > 20:
|
||||
assessment = "SIGNIFICANTLY UNDERVALUED"
|
||||
elif value_gap > 0:
|
||||
assessment = "POTENTIALLY UNDERVALUED"
|
||||
elif value_gap > -20:
|
||||
assessment = "POTENTIALLY OVERVALUED"
|
||||
else:
|
||||
assessment = "SIGNIFICANTLY OVERVALUED"
|
||||
|
||||
lines.append(f"{ticker}:")
|
||||
lines.append(f" Current FCF: ${current_fcf:,.0f}")
|
||||
lines.append(f" DCF Enterprise Value: ${enterprise_value:,.0f}")
|
||||
lines.append(f" Market Cap: ${market_cap:,.0f}")
|
||||
lines.append(f" Value Gap: {value_gap:+.1f}% -> {assessment}")
|
||||
lines.append("")
|
||||
|
||||
return _to_text_response("\n".join(lines))
|
||||
return _to_text_response(build_dcf_report(rows, current_date))
|
||||
|
||||
|
||||
@safe
|
||||
@@ -911,7 +893,7 @@ def owner_earnings_valuation_analysis(
|
||||
|
||||
current_date = _resolved_date(current_date)
|
||||
tickers = _parse_tickers(tickers)
|
||||
lines = [f"=== Owner Earnings Valuation ({current_date}) ===\n"]
|
||||
rows = []
|
||||
|
||||
for ticker in tickers:
|
||||
metrics = get_financial_metrics(
|
||||
@@ -920,7 +902,7 @@ def owner_earnings_valuation_analysis(
|
||||
limit=8,
|
||||
)
|
||||
if not metrics:
|
||||
lines.append(f"{ticker}: No financial metrics\n")
|
||||
rows.append({"ticker": ticker, "error": "No financial metrics"})
|
||||
continue
|
||||
|
||||
line_items = search_line_items(
|
||||
@@ -936,12 +918,12 @@ def owner_earnings_valuation_analysis(
|
||||
limit=2,
|
||||
)
|
||||
if len(line_items) < 2:
|
||||
lines.append(f"{ticker}: Insufficient financial data\n")
|
||||
rows.append({"ticker": ticker, "error": "Insufficient financial data"})
|
||||
continue
|
||||
|
||||
market_cap = get_market_cap(ticker, current_date)
|
||||
if not market_cap:
|
||||
lines.append(f"{ticker}: Market cap unavailable\n")
|
||||
rows.append({"ticker": ticker, "error": "Market cap unavailable"})
|
||||
continue
|
||||
|
||||
m = metrics[0]
|
||||
@@ -956,57 +938,27 @@ def owner_earnings_valuation_analysis(
|
||||
|
||||
owner_earnings = net_income + depreciation - capex - wc_change
|
||||
if owner_earnings <= 0:
|
||||
lines.append(
|
||||
f"{ticker}: Negative owner earnings (${owner_earnings:,.0f})\n",
|
||||
rows.append(
|
||||
{
|
||||
"ticker": ticker,
|
||||
"error": f"Negative owner earnings (${owner_earnings:,.0f})",
|
||||
},
|
||||
)
|
||||
continue
|
||||
|
||||
# Valuation
|
||||
growth_rate = m.earnings_growth or 0.05
|
||||
required_return = 0.15
|
||||
margin_of_safety = 0.25
|
||||
num_years = 5
|
||||
|
||||
pv_earnings = sum(
|
||||
owner_earnings
|
||||
* (1 + growth_rate) ** year
|
||||
/ (1 + required_return) ** year
|
||||
for year in range(1, num_years + 1)
|
||||
rows.append(
|
||||
{
|
||||
"ticker": ticker,
|
||||
"owner_earnings": owner_earnings,
|
||||
"growth_rate": m.earnings_growth or 0.05,
|
||||
"market_cap": market_cap,
|
||||
"required_return": 0.15,
|
||||
"margin_of_safety": 0.25,
|
||||
"num_years": 5,
|
||||
},
|
||||
)
|
||||
terminal_growth = min(growth_rate, 0.03)
|
||||
terminal_earnings = (
|
||||
owner_earnings
|
||||
* (1 + growth_rate) ** num_years
|
||||
* (1 + terminal_growth)
|
||||
)
|
||||
terminal_value = terminal_earnings / (
|
||||
required_return - terminal_growth
|
||||
)
|
||||
pv_terminal = terminal_value / (1 + required_return) ** num_years
|
||||
|
||||
intrinsic_value = (pv_earnings + pv_terminal) * (1 - margin_of_safety)
|
||||
value_gap = (intrinsic_value - market_cap) / market_cap * 100
|
||||
|
||||
# Assessment
|
||||
if value_gap > 20:
|
||||
assessment = "SIGNIFICANTLY UNDERVALUED"
|
||||
elif value_gap > 0:
|
||||
assessment = "POTENTIALLY UNDERVALUED"
|
||||
elif value_gap > -20:
|
||||
assessment = "POTENTIALLY OVERVALUED"
|
||||
else:
|
||||
assessment = "SIGNIFICANTLY OVERVALUED"
|
||||
|
||||
lines.append(f"{ticker}:")
|
||||
lines.append(f" Owner Earnings: ${owner_earnings:,.0f}")
|
||||
lines.append(
|
||||
f" Intrinsic Value (w/ 25% MoS): ${intrinsic_value:,.0f}",
|
||||
)
|
||||
lines.append(f" Market Cap: ${market_cap:,.0f}")
|
||||
lines.append(f" Value Gap: {value_gap:+.1f}% -> {assessment}")
|
||||
lines.append("")
|
||||
|
||||
return _to_text_response("\n".join(lines))
|
||||
return _to_text_response(build_owner_earnings_report(rows, current_date))
|
||||
|
||||
|
||||
@safe
|
||||
@@ -1030,7 +982,7 @@ def ev_ebitda_valuation_analysis(
|
||||
|
||||
current_date = _resolved_date(current_date)
|
||||
tickers = _parse_tickers(tickers)
|
||||
lines = [f"=== EV/EBITDA Valuation ({current_date}) ===\n"]
|
||||
rows = []
|
||||
|
||||
for ticker in tickers:
|
||||
metrics = get_financial_metrics(
|
||||
@@ -1039,7 +991,7 @@ def ev_ebitda_valuation_analysis(
|
||||
limit=8,
|
||||
)
|
||||
if not metrics:
|
||||
lines.append(f"{ticker}: No financial metrics\n")
|
||||
rows.append({"ticker": ticker, "error": "No financial metrics"})
|
||||
continue
|
||||
|
||||
m = metrics[0]
|
||||
@@ -1048,12 +1000,12 @@ def ev_ebitda_valuation_analysis(
|
||||
or not m.enterprise_value_to_ebitda_ratio
|
||||
or m.enterprise_value_to_ebitda_ratio <= 0
|
||||
):
|
||||
lines.append(f"{ticker}: Missing EV/EBITDA data\n")
|
||||
rows.append({"ticker": ticker, "error": "Missing EV/EBITDA data"})
|
||||
continue
|
||||
|
||||
market_cap = get_market_cap(ticker, current_date)
|
||||
if not market_cap:
|
||||
lines.append(f"{ticker}: Market cap unavailable\n")
|
||||
rows.append({"ticker": ticker, "error": "Market cap unavailable"})
|
||||
continue
|
||||
|
||||
current_ebitda = (
|
||||
@@ -1067,42 +1019,21 @@ def ev_ebitda_valuation_analysis(
|
||||
and x.enterprise_value_to_ebitda_ratio > 0
|
||||
]
|
||||
if len(valid_multiples) < 3:
|
||||
lines.append(f"{ticker}: Insufficient historical data\n")
|
||||
rows.append({"ticker": ticker, "error": "Insufficient historical data"})
|
||||
continue
|
||||
|
||||
median_multiple = median(valid_multiples)
|
||||
current_multiple = m.enterprise_value_to_ebitda_ratio
|
||||
|
||||
implied_ev = median_multiple * current_ebitda
|
||||
net_debt = m.enterprise_value - market_cap
|
||||
implied_equity = max(implied_ev - net_debt, 0)
|
||||
|
||||
value_gap = (
|
||||
(implied_equity - market_cap) / market_cap * 100
|
||||
if market_cap > 0
|
||||
else 0
|
||||
)
|
||||
multiple_discount = (
|
||||
(median_multiple - current_multiple) / median_multiple * 100
|
||||
rows.append(
|
||||
{
|
||||
"ticker": ticker,
|
||||
"current_multiple": m.enterprise_value_to_ebitda_ratio,
|
||||
"median_multiple": median(valid_multiples),
|
||||
"current_ebitda": current_ebitda,
|
||||
"market_cap": market_cap,
|
||||
"net_debt": m.enterprise_value - market_cap,
|
||||
},
|
||||
)
|
||||
|
||||
# Assessment
|
||||
if multiple_discount > 10:
|
||||
assessment = "TRADING BELOW HISTORICAL MULTIPLE"
|
||||
elif multiple_discount > -10:
|
||||
assessment = "NEAR HISTORICAL AVERAGE"
|
||||
else:
|
||||
assessment = "TRADING ABOVE HISTORICAL MULTIPLE"
|
||||
|
||||
lines.append(f"{ticker}:")
|
||||
lines.append(f" Current EV/EBITDA: {current_multiple:.1f}x")
|
||||
lines.append(f" Historical Median: {median_multiple:.1f}x")
|
||||
lines.append(f" Multiple vs History: {multiple_discount:+.1f}%")
|
||||
lines.append(f" Implied Equity Value: ${implied_equity:,.0f}")
|
||||
lines.append(f" Value Gap: {value_gap:+.1f}% -> {assessment}")
|
||||
lines.append("")
|
||||
|
||||
return _to_text_response("\n".join(lines))
|
||||
return _to_text_response(build_ev_ebitda_report(rows, current_date))
|
||||
|
||||
|
||||
@safe
|
||||
@@ -1126,7 +1057,7 @@ def residual_income_valuation_analysis(
|
||||
|
||||
current_date = _resolved_date(current_date)
|
||||
tickers = _parse_tickers(tickers)
|
||||
lines = [f"=== Residual Income Valuation ({current_date}) ===\n"]
|
||||
rows = []
|
||||
|
||||
for ticker in tickers:
|
||||
metrics = get_financial_metrics(
|
||||
@@ -1135,7 +1066,7 @@ def residual_income_valuation_analysis(
|
||||
limit=8,
|
||||
)
|
||||
if not metrics:
|
||||
lines.append(f"{ticker}: No financial metrics\n")
|
||||
rows.append({"ticker": ticker, "error": "No financial metrics"})
|
||||
continue
|
||||
|
||||
line_items = search_line_items(
|
||||
@@ -1146,59 +1077,44 @@ def residual_income_valuation_analysis(
|
||||
limit=1,
|
||||
)
|
||||
if not line_items or not line_items[0].net_income:
|
||||
lines.append(f"{ticker}: No net income data\n")
|
||||
rows.append({"ticker": ticker, "error": "No net income data"})
|
||||
continue
|
||||
|
||||
market_cap = get_market_cap(ticker, current_date)
|
||||
if not market_cap:
|
||||
lines.append(f"{ticker}: Market cap unavailable\n")
|
||||
rows.append({"ticker": ticker, "error": "Market cap unavailable"})
|
||||
continue
|
||||
|
||||
m = metrics[0]
|
||||
if not m.price_to_book_ratio or m.price_to_book_ratio <= 0:
|
||||
lines.append(f"{ticker}: Invalid P/B ratio\n")
|
||||
rows.append({"ticker": ticker, "error": "Invalid P/B ratio"})
|
||||
continue
|
||||
|
||||
net_income = line_items[0].net_income
|
||||
pb_ratio = m.price_to_book_ratio
|
||||
book_value = market_cap / pb_ratio
|
||||
|
||||
# Model parameters
|
||||
cost_of_equity = 0.10
|
||||
bv_growth = m.book_value_growth or 0.03
|
||||
terminal_growth = 0.03
|
||||
num_years = 5
|
||||
margin_of_safety = 0.20
|
||||
|
||||
initial_ri = net_income - cost_of_equity * book_value
|
||||
if initial_ri <= 0:
|
||||
lines.append(f"{ticker}: Negative residual income\n")
|
||||
rows.append({"ticker": ticker, "error": "Negative residual income"})
|
||||
continue
|
||||
|
||||
# PV calculation
|
||||
pv_ri = sum(
|
||||
initial_ri * (1 + bv_growth) ** year / (1 + cost_of_equity) ** year
|
||||
for year in range(1, num_years + 1)
|
||||
rows.append(
|
||||
{
|
||||
"ticker": ticker,
|
||||
"book_value": book_value,
|
||||
"initial_ri": initial_ri,
|
||||
"market_cap": market_cap,
|
||||
"cost_of_equity": cost_of_equity,
|
||||
"bv_growth": m.book_value_growth or 0.03,
|
||||
"terminal_growth": 0.03,
|
||||
"num_years": 5,
|
||||
"margin_of_safety": 0.20,
|
||||
},
|
||||
)
|
||||
terminal_ri = initial_ri * (1 + bv_growth) ** (num_years + 1)
|
||||
terminal_value = terminal_ri / (cost_of_equity - terminal_growth)
|
||||
pv_terminal = terminal_value / (1 + cost_of_equity) ** num_years
|
||||
|
||||
intrinsic_value = (book_value + pv_ri + pv_terminal) * (
|
||||
1 - margin_of_safety
|
||||
)
|
||||
value_gap = (intrinsic_value - market_cap) / market_cap * 100
|
||||
|
||||
lines.append(f"{ticker}:")
|
||||
lines.append(f" Book Value: ${book_value:,.0f}")
|
||||
lines.append(f" Residual Income: ${initial_ri:,.0f}")
|
||||
lines.append(
|
||||
f" Intrinsic Value (w/ 20% MoS): ${intrinsic_value:,.0f}",
|
||||
)
|
||||
lines.append(f" Value Gap: {value_gap:+.1f}%")
|
||||
lines.append("")
|
||||
|
||||
return _to_text_response("\n".join(lines))
|
||||
return _to_text_response(build_residual_income_report(rows, current_date))
|
||||
|
||||
|
||||
# Tool Registry for dynamic toolkit creation
|
||||
|
||||
Reference in New Issue
Block a user