Add per-agent skill workspaces and TraderView management
This commit is contained in:
@@ -1,21 +1,31 @@
|
||||
---
|
||||
name: valuation_review
|
||||
description: Estimate fair value and margin of safety using multiple valuation lenses.
|
||||
name: 估值分析
|
||||
description: 当用户要求“估值分析”“看合理价值”“判断高估低估”“测算安全边际”或“比较多种估值方法”时,应使用此技能。
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Valuation Review
|
||||
# 估值分析
|
||||
|
||||
Use this skill when the task requires determining whether a stock is cheap, expensive, or fairly priced.
|
||||
当用户需要判断一只股票是低估、高估还是定价合理时,使用这个技能。
|
||||
|
||||
## Workflow
|
||||
## 工作流程
|
||||
|
||||
1. Use more than one valuation method when possible.
|
||||
2. Compare intrinsic value estimates with current market pricing.
|
||||
3. Explain the key assumptions behind the valuation view.
|
||||
4. State the margin of safety and what could compress or expand it.
|
||||
1. 条件允许时,使用不止一种估值方法。
|
||||
2. 对比内在价值估计与当前市场价格。
|
||||
3. 解释估值判断背后的关键假设。
|
||||
4. 明确安全边际,以及哪些因素会压缩或扩大它。
|
||||
|
||||
## Guardrails
|
||||
## 可复用资源
|
||||
|
||||
- Treat valuation as a range, not a single precise number.
|
||||
- Call out assumption sensitivity.
|
||||
- Avoid high-confidence calls when inputs are sparse or unstable.
|
||||
- `scripts/dcf_report.py`
|
||||
用于贴现现金流估值的确定性计算和报告生成。
|
||||
- `scripts/owner_earnings_report.py`
|
||||
用于 owner earnings 估值的确定性计算和报告生成。
|
||||
- `scripts/multiple_valuation_report.py`
|
||||
用于 EV/EBITDA 和 Residual Income 两类估值报告生成。
|
||||
|
||||
## 约束
|
||||
|
||||
- 将估值视为区间,而不是一个精确点值。
|
||||
- 明确说明假设敏感性。
|
||||
- 当输入稀疏或不稳定时,避免给出高置信度判断。
|
||||
|
||||
1
backend/skills/builtin/valuation_review/__init__.py
Normal file
1
backend/skills/builtin/valuation_review/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Deterministic DCF report helpers for the valuation_review skill."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
def build_dcf_report(rows: Iterable[dict], current_date: str) -> str:
|
||||
"""Render a DCF valuation report from normalized row inputs."""
|
||||
lines = [f"=== DCF Valuation Analysis ({current_date}) ===\n"]
|
||||
|
||||
for row in rows:
|
||||
error = row.get("error")
|
||||
ticker = row["ticker"]
|
||||
if error:
|
||||
lines.append(f"{ticker}: {error}\n")
|
||||
continue
|
||||
|
||||
current_fcf = float(row["current_fcf"])
|
||||
growth_rate = float(row["growth_rate"])
|
||||
market_cap = float(row["market_cap"])
|
||||
discount_rate = float(row.get("discount_rate", 0.10))
|
||||
terminal_growth = float(row.get("terminal_growth", 0.03))
|
||||
num_years = int(row.get("num_years", 5))
|
||||
|
||||
pv_fcf = sum(
|
||||
current_fcf
|
||||
* (1 + growth_rate) ** year
|
||||
/ (1 + discount_rate) ** year
|
||||
for year in range(1, num_years + 1)
|
||||
)
|
||||
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
|
||||
|
||||
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 "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Read normalized rows from stdin and emit a text report."""
|
||||
payload = json.load(__import__("sys").stdin)
|
||||
print(build_dcf_report(payload["rows"], payload["current_date"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Deterministic multiple-based valuation helpers for the valuation_review skill."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
def build_ev_ebitda_report(rows: Iterable[dict], current_date: str) -> str:
|
||||
"""Render an EV/EBITDA valuation report from normalized row inputs."""
|
||||
lines = [f"=== EV/EBITDA Valuation ({current_date}) ===\n"]
|
||||
|
||||
for row in rows:
|
||||
error = row.get("error")
|
||||
ticker = row["ticker"]
|
||||
if error:
|
||||
lines.append(f"{ticker}: {error}\n")
|
||||
continue
|
||||
|
||||
current_multiple = float(row["current_multiple"])
|
||||
median_multiple = float(row["median_multiple"])
|
||||
current_ebitda = float(row["current_ebitda"])
|
||||
market_cap = float(row["market_cap"])
|
||||
net_debt = float(row["net_debt"])
|
||||
|
||||
implied_ev = median_multiple * current_ebitda
|
||||
implied_equity = max(implied_ev - net_debt, 0.0)
|
||||
value_gap = (
|
||||
(implied_equity - market_cap) / market_cap * 100
|
||||
if market_cap > 0
|
||||
else 0.0
|
||||
)
|
||||
multiple_discount = (
|
||||
(median_multiple - current_multiple) / median_multiple * 100
|
||||
)
|
||||
|
||||
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 "\n".join(lines)
|
||||
|
||||
|
||||
def build_residual_income_report(rows: Iterable[dict], current_date: str) -> str:
|
||||
"""Render a residual income valuation report from normalized row inputs."""
|
||||
lines = [f"=== Residual Income Valuation ({current_date}) ===\n"]
|
||||
|
||||
for row in rows:
|
||||
error = row.get("error")
|
||||
ticker = row["ticker"]
|
||||
if error:
|
||||
lines.append(f"{ticker}: {error}\n")
|
||||
continue
|
||||
|
||||
book_value = float(row["book_value"])
|
||||
initial_ri = float(row["initial_ri"])
|
||||
market_cap = float(row["market_cap"])
|
||||
cost_of_equity = float(row.get("cost_of_equity", 0.10))
|
||||
bv_growth = float(row.get("bv_growth", 0.03))
|
||||
terminal_growth = float(row.get("terminal_growth", 0.03))
|
||||
num_years = int(row.get("num_years", 5))
|
||||
margin_of_safety = float(row.get("margin_of_safety", 0.20))
|
||||
|
||||
pv_ri = sum(
|
||||
initial_ri * (1 + bv_growth) ** year / (1 + cost_of_equity) ** year
|
||||
for year in range(1, num_years + 1)
|
||||
)
|
||||
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 "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Read normalized rows from stdin and emit one selected text report."""
|
||||
payload = json.load(__import__("sys").stdin)
|
||||
mode = payload["mode"]
|
||||
if mode == "ev_ebitda":
|
||||
print(build_ev_ebitda_report(payload["rows"], payload["current_date"]))
|
||||
return
|
||||
if mode == "residual_income":
|
||||
print(build_residual_income_report(payload["rows"], payload["current_date"]))
|
||||
return
|
||||
raise ValueError(f"Unsupported mode: {mode}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Deterministic owner earnings valuation helpers for the valuation_review skill."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
def build_owner_earnings_report(rows: Iterable[dict], current_date: str) -> str:
|
||||
"""Render an owner earnings valuation report from normalized row inputs."""
|
||||
lines = [f"=== Owner Earnings Valuation ({current_date}) ===\n"]
|
||||
|
||||
for row in rows:
|
||||
error = row.get("error")
|
||||
ticker = row["ticker"]
|
||||
if error:
|
||||
lines.append(f"{ticker}: {error}\n")
|
||||
continue
|
||||
|
||||
owner_earnings = float(row["owner_earnings"])
|
||||
growth_rate = float(row["growth_rate"])
|
||||
market_cap = float(row["market_cap"])
|
||||
required_return = float(row.get("required_return", 0.15))
|
||||
margin_of_safety = float(row.get("margin_of_safety", 0.25))
|
||||
num_years = int(row.get("num_years", 5))
|
||||
|
||||
pv_earnings = sum(
|
||||
owner_earnings
|
||||
* (1 + growth_rate) ** year
|
||||
/ (1 + required_return) ** year
|
||||
for year in range(1, num_years + 1)
|
||||
)
|
||||
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
|
||||
|
||||
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 "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Read normalized rows from stdin and emit a text report."""
|
||||
payload = json.load(__import__("sys").stdin)
|
||||
print(build_owner_earnings_report(payload["rows"], payload["current_date"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user