test/skills/stock-analysis-skill/src/dividend.ts
2026-03-24 04:04:58 +00:00

227 lines
9.7 KiB
TypeScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* dividend.ts
* 股息分析模块(适配 finance skill
* 移植原版评分逻辑:安全评分 / CAGR / 连续增长年数 / 派息可持续性
*/
import ZAI from "z-ai-web-dev-sdk";
import { DividendAnalysis, PayoutStatus, IncomeRating } from "./types";
// ── 核心评分逻辑(与原版完全一致)──────────────────────────
function calcPayoutStatus(payoutRatio: number | null): PayoutStatus {
if (payoutRatio === null) return "unknown";
if (payoutRatio < 40) return "safe";
if (payoutRatio < 60) return "moderate";
if (payoutRatio < 80) return "high";
return "unsustainable";
}
function calcSafetyScore(data: {
payoutRatio: number | null;
dividendGrowth5y: number | null;
consecutiveYears: number | null;
dividendYield: number | null;
}): { score: number; factors: string[] } {
let score = 50;
const factors: string[] = [];
// 派息率±20
if (data.payoutRatio !== null) {
if (data.payoutRatio < 40) { score += 20; factors.push(`派息率健康(${data.payoutRatio.toFixed(0)}%`); }
else if (data.payoutRatio < 60) { score += 10; factors.push(`派息率适中(${data.payoutRatio.toFixed(0)}%`); }
else if (data.payoutRatio < 80) { score -= 10; factors.push(`派息率偏高(${data.payoutRatio.toFixed(0)}%`); }
else { score -= 20; factors.push(`派息率不可持续(${data.payoutRatio.toFixed(0)}%`); }
}
// 5年增长率±15
if (data.dividendGrowth5y !== null) {
if (data.dividendGrowth5y > 10) { score += 15; factors.push(`股息增长强劲(${data.dividendGrowth5y.toFixed(1)}% CAGR`); }
else if (data.dividendGrowth5y > 5) { score += 10; factors.push(`股息增长良好(${data.dividendGrowth5y.toFixed(1)}% CAGR`); }
else if (data.dividendGrowth5y > 0) { score += 5; factors.push(`股息小幅增长(${data.dividendGrowth5y.toFixed(1)}% CAGR`); }
else { score -= 15; factors.push(`股息下降(${data.dividendGrowth5y.toFixed(1)}% CAGR`); }
}
// 连续增长年数±15
if (data.consecutiveYears !== null) {
if (data.consecutiveYears >= 25) { score += 15; factors.push(`股息贵族(连续${data.consecutiveYears}年增长)`); }
else if (data.consecutiveYears >= 10) { score += 10; factors.push(`长期稳定股息(${data.consecutiveYears}年)`); }
else if (data.consecutiveYears >= 5) { score += 5; factors.push(`股息稳定(${data.consecutiveYears}年)`); }
}
// 高收益率风险(-10
if (data.dividendYield !== null) {
if (data.dividendYield > 8) { score -= 10; factors.push(`收益率过高(${data.dividendYield.toFixed(1)}%),需核实可持续性`); }
else if (data.dividendYield < 1) { factors.push(`收益率偏低(${data.dividendYield.toFixed(2)}%`); }
}
return { score: Math.max(0, Math.min(100, score)), factors };
}
function calcIncomeRating(safetyScore: number): IncomeRating {
if (safetyScore >= 80) return "excellent";
if (safetyScore >= 60) return "good";
if (safetyScore >= 40) return "moderate";
return "poor";
}
// ── 通过 finance skill 获取股息数据 ──────────────────────
async function fetchDividendData(ticker: string): Promise<Record<string, any>> {
const zai = await ZAI.create();
const completion = await zai.chat.completions.create({
messages: [{
role: "user",
content: `请查询 ${ticker} 的股息数据,只返回 JSON包含
name公司名称, currentPrice当前股价,
dividendYield年化股息率%, annualDividend年度每股股息,
trailingEps过去12月每股收益,
exDividendDate除权日 YYYY-MM-DD 格式),
paymentFrequency"monthly"/"quarterly"/"annual",根据派息频率判断),
dividendHistory近5年年度股息数组每项含 year 和 total从新到旧排序,
consecutiveYears连续股息增长年数整数,
dividendGrowth5y近5年股息 CAGR %)。
缺失字段填 null不得捏造。`,
}],
thinking: { type: "disabled" },
});
const raw = completion.choices[0]?.message?.content ?? "{}";
return JSON.parse(raw.replace(/```json|```/g, "").trim());
}
// ── 主分析函数 ────────────────────────────────────────────
export async function analyzeDividend(ticker: string): Promise<DividendAnalysis> {
ticker = ticker.toUpperCase();
let raw: Record<string, any> = {};
try {
raw = await fetchDividendData(ticker);
} catch (err: any) {
return {
ticker, name: ticker, currentPrice: null,
dividendYield: null, annualDividend: null,
payoutRatio: null, payoutStatus: "unknown",
dividendGrowth5y: null, consecutiveYears: null,
exDividendDate: null, paymentFrequency: null,
safetyScore: 0, safetyFactors: [`数据获取失败:${err.message}`],
incomeRating: "poor", dividendHistory: [],
summary: `${ticker} 股息数据获取失败。`,
};
}
// 无股息
if (!raw.annualDividend || raw.annualDividend === 0) {
return {
ticker, name: raw.name ?? ticker,
currentPrice: raw.currentPrice ?? null,
dividendYield: null, annualDividend: null,
payoutRatio: null, payoutStatus: "no_dividend",
dividendGrowth5y: null, consecutiveYears: null,
exDividendDate: null, paymentFrequency: null,
safetyScore: 0, safetyFactors: ["该股票不派息"],
incomeRating: "no_dividend", dividendHistory: [],
summary: `${ticker} 目前不派发股息。`,
};
}
// 计算派息率
const payoutRatio = (raw.trailingEps && raw.trailingEps > 0 && raw.annualDividend)
? parseFloat(((raw.annualDividend / raw.trailingEps) * 100).toFixed(1))
: null;
const payoutStatus = calcPayoutStatus(payoutRatio);
const { score: safetyScore, factors: safetyFactors } = calcSafetyScore({
payoutRatio,
dividendGrowth5y: raw.dividendGrowth5y ?? null,
consecutiveYears: raw.consecutiveYears ?? null,
dividendYield: raw.dividendYield ?? null,
});
const incomeRating = calcIncomeRating(safetyScore);
// 生成摘要
const parts: string[] = [];
if (raw.dividendYield) parts.push(`收益率 ${Number(raw.dividendYield).toFixed(2)}%`);
if (payoutRatio) parts.push(`派息率 ${payoutRatio.toFixed(0)}%`);
if (raw.dividendGrowth5y) parts.push(`5年增长 ${Number(raw.dividendGrowth5y) > 0 ? "+" : ""}${Number(raw.dividendGrowth5y).toFixed(1)}%`);
if (raw.consecutiveYears && raw.consecutiveYears >= 5) parts.push(`连续增长 ${raw.consecutiveYears}`);
const ratingLabel: Record<IncomeRating, string> = {
excellent: "优秀", good: "良好", moderate: "一般", poor: "较差", no_dividend: "无股息",
};
return {
ticker,
name: raw.name ?? ticker,
currentPrice: raw.currentPrice ?? null,
dividendYield: raw.dividendYield ? Number(Number(raw.dividendYield).toFixed(2)) : null,
annualDividend: raw.annualDividend ?? null,
payoutRatio,
payoutStatus,
dividendGrowth5y: raw.dividendGrowth5y ? Number(Number(raw.dividendGrowth5y).toFixed(2)) : null,
consecutiveYears: raw.consecutiveYears ?? null,
exDividendDate: raw.exDividendDate ?? null,
paymentFrequency: raw.paymentFrequency ?? null,
safetyScore,
safetyFactors,
incomeRating,
dividendHistory: raw.dividendHistory ?? [],
summary: `${ticker}${raw.name ?? ""}${parts.join("")}。评级:${ratingLabel[incomeRating]}`,
};
}
// ── 格式化输出Markdown 仪表盘)─────────────────────────
export function formatDividendMarkdown(analysis: DividendAnalysis): string {
if (analysis.incomeRating === "no_dividend") {
return `### 💰 股息分析 · ${analysis.ticker}\n\n该股票目前不派发股息。`;
}
const ratingEmoji: Record<IncomeRating, string> = {
excellent: "🏆", good: "✅", moderate: "⚠️", poor: "❌", no_dividend: "—",
};
const payoutLabel: Record<string, string> = {
safe: "✅ 安全", moderate: "⚠️ 适中", high: "⚠️ 偏高", unsustainable: "❌ 不可持续", unknown: "暂缺",
};
let md = `### 💰 股息分析 · ${analysis.ticker}${analysis.name}
| 指标 | 数值 |
|------|------|
| 股息收益率 | ${analysis.dividendYield ? `${analysis.dividendYield}%` : "暂缺"} |
| 年度每股股息 | ${analysis.annualDividend ? `$${analysis.annualDividend}` : "暂缺"} |
| 派息频率 | ${analysis.paymentFrequency ?? "暂缺"} |
| 除权日 | ${analysis.exDividendDate ?? "暂缺"} |
| 派息率 | ${analysis.payoutRatio ? `${analysis.payoutRatio}%${payoutLabel[analysis.payoutStatus]}` : "暂缺"} |
| 5年股息增长 | ${analysis.dividendGrowth5y ? `${analysis.dividendGrowth5y > 0 ? "+" : ""}${analysis.dividendGrowth5y}%` : "暂缺"} |
| 连续增长年数 | ${analysis.consecutiveYears ?? "暂缺"} |
**安全评分:${analysis.safetyScore}/100 ${ratingEmoji[analysis.incomeRating]} 收入评级:${analysis.incomeRating.toUpperCase()}**
评分依据:
${analysis.safetyFactors.map((f) => `- ${f}`).join("\n")}
`;
if (analysis.dividendHistory.length > 0) {
md += `\n近年股息历史\n`;
md += analysis.dividendHistory.slice(0, 5).map((h) => `- ${h.year}年:$${h.total}`).join("\n");
}
return md;
}
// ── 批量分析 ──────────────────────────────────────────────
export async function analyzeDividends(tickers: string[]): Promise<DividendAnalysis[]> {
const results: DividendAnalysis[] = [];
for (const ticker of tickers) {
results.push(await analyzeDividend(ticker));
await new Promise((r) => setTimeout(r, 200));
}
return results;
}