Add per-agent skill workspaces and TraderView management

This commit is contained in:
2026-03-17 13:55:14 +08:00
parent 1f5ee3698e
commit 2daf5717ba
35 changed files with 4774 additions and 331 deletions

View File

@@ -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