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

265 lines
9.1 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

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.

/**
* analyzer.ts — LLM/VLM 分析层
* 七段式决策仪表盘 + 美股可附加股息分析
*/
import ZAI from "z-ai-web-dev-sdk";
import { StockData, AnalysisResult, OutputFormat, Market, Verdict, PositionInfo } from "./types";
import { validateStockData } from "./dataFetcher";
import { analyzeDividend, formatDividendMarkdown } from "./dividend";
const MARKET_LABEL: Record<Market, string> = { CN: "A股", HK: "港股", US: "美股" };
// ── 仪表盘 Prompt ─────────────────────────────────────────
function buildDashboardPrompt(
data: StockData,
position: PositionInfo | undefined,
warnings: string[]
): string {
const warningBlock = warnings.length > 0
? `⚠️ 数据预警(必须在报告中体现):\n${warnings.map((w) => `- ${w}`).join("\n")}\n\n`
: "";
const positionBlock = position
? position.status === "holding"
? `用户持仓:持仓中,成本价 ${position.cost ?? "未知"}${position.shares ? `${position.shares}` : ""}。请给出盈亏分析和针对性建议。`
: `用户持仓:当前空仓。`
: `用户持仓:未提供(请同时给出空仓者和持仓者两套建议)。`;
return `${warningBlock}${positionBlock}
股票数据:
\`\`\`json
${JSON.stringify(data, null, 2)}
\`\`\`
请输出以下格式的完整决策仪表盘(严格按结构,不增删章节):
---
## 决策仪表盘 · {名称}({代码}) · {市场}
---
### 📰 重要信息速览
**💭 舆情情绪:** 一句话描述
**📊 业绩预期:** 结合 PE/ROE/行业简述,数据缺失标"暂缺"
**🚨 风险警报:**
- 风险1技术面或宏观
- 风险2基本面或行业
**✨ 利好催化:**
- 利好1技术面
- 利好2基本面或行业
**📢 最新动态:** 结合行业背景补充1条关键信息
---
### 📌 核心结论
**[emoji] 结论:强烈买入 / 买入 / 观望 / 卖出**(四选一,乖离率>5%不得为买入)
**💬 一句话决策:** 核心逻辑
**⏰ 时效性:** 立即行动 / 今日内 / 不急
(根据持仓状态输出)
- **🆕 空仓者:** 是否进场、建仓点位、仓位比例
- **💼 持仓者:** 持有/加仓/减仓/止损建议${position?.status === "holding" && position.cost ? ",含成本盈亏分析" : ""}
---
### 📈 当日行情
列出:收盘价、昨收、开盘、最高、最低、涨跌幅、涨跌额、振幅、成交量、成交额(缺失标"暂缺"
---
### 📊 数据透视
**技术面:**
表格(指标 | 数值 | 解读MA5、MA10、MA20、乖离率(BIAS20)、RSI如有、支撑位、压力位
结论:均线状态 + 趋势强度xx/100
**基本面(注明报告期):**
表格(指标 | 数值 | 行业对比ROE、毛利率、净利率、资产负债率、PE、PB
数据缺失标"暂缺",不得捏造
**资金面:**A股/港股适用,美股可略)
- 主力净流入:金额(占比%),一句话解读
- 筹码:获利比例 | 平均成本 | 集中度
---
### 🎯 作战计划
| 点位类型 | 价格 | 说明 |
|---------|------|------|
| 🎯 理想买入 | xxx | |
| 🔵 次优买入 | xxx | |
| 🛑 止损位 | xxx | |
| 🎊 目标位 | xxx | |
**💰 仓位建议:** x成
**建仓策略:** 分批策略
**风控策略:** 止损纪律
---
### ✅ 检查清单
- ✅/⚠️/❌ 均线状态
- ✅/⚠️/❌ 乖离率安全(<5%
- ✅/⚠️/❌ 量能配合
- ✅/⚠️/❌ 估值合理
- ✅/⚠️/❌ 资金流向
- ✅/⚠️/❌ 筹码健康
**综合结论:** 一句话总结当前状态和建议。
---
*以上分析仅供参考,不构成投资建议,据此操作风险自担。*`;
}
// ── 研报 PromptPDF/Word──────────────────────────────
function buildReportPrompt(data: StockData, position: PositionInfo | undefined): string {
const positionBlock = position?.status === "holding"
? `用户持仓成本:${position.cost ?? "未知"}`
: "用户当前空仓";
return `${positionBlock}
股票数据:
\`\`\`json
${JSON.stringify(data, null, 2)}
\`\`\`
请生成结构化研报:
【研究报告】{名称}({代码}) · {市场} · {日期}
一、投资结论(买入/强烈买入/观望/卖出,含目标价、止损价,分空仓/持仓两套建议)
二、重要信息速览(舆情/业绩预期/风险/利好/最新动态)
三、数据透视(技术面/基本面/资金面)
四、作战计划(点位表/仓位/持仓周期)
五、风险提示2-3条
免责声明本报告由AI辅助生成仅供参考不构成投资建议据此操作风险自担。`;
}
// ── 提取结论 ──────────────────────────────────────────────
function extractVerdict(text: string): Verdict {
const patterns = [
/结论[:]\s*[💚🟢🟡🔴⚪]?\s*(强烈买入|买入|观望|卖出)/,
/核心结论[:]\s*[💚🟢🟡🔴⚪]?\s*(强烈买入|买入|观望|卖出)/,
/\*\*(强烈买入|买入|观望|卖出)\*\*/,
];
for (const p of patterns) {
const m = text.match(p);
if (m) return m[1] as Verdict;
}
return "观望";
}
// ── 核心分析 ──────────────────────────────────────────────
export async function analyzeStock(
data: StockData,
outputFormat: OutputFormat = "markdown",
position?: PositionInfo,
includeDividend = false
): Promise<AnalysisResult> {
const { valid, warnings } = validateStockData(data);
const name = data.name ?? data.code;
if (!valid) {
return {
code: data.code, market: data.market, name,
verdict: "观望",
analysis: `## ⚠️ 数据获取失败\n\n${data.code} 数据无法获取(${data.error ?? "未知错误"}),建议手动核实。`,
warnings, outputFormat,
generatedAt: new Date().toISOString(),
};
}
const zai = await ZAI.create();
const userPrompt = outputFormat === "markdown"
? buildDashboardPrompt(data, position, warnings)
: buildReportPrompt(data, position);
let analysisText = "⚠️ LLM 未返回内容,请重试。";
try {
const completion = await zai.chat.completions.create({
messages: [
{ role: "system", content: `你是一位资深${MARKET_LABEL[data.market]}股票分析师。数据缺失标"暂缺",严禁捏造。乖离率>5%不得建议买入。结论四选一:强烈买入/买入/观望/卖出。输出语言:中文。` },
{ role: "user", content: userPrompt },
],
thinking: { type: "disabled" },
});
analysisText = completion.choices[0]?.message?.content ?? analysisText;
} catch (err: any) {
analysisText = `## ⚠️ 分析失败\n\nLLM 调用出错:${err.message}`;
}
// 美股附加股息分析
if (includeDividend && data.market === "US" && outputFormat === "markdown") {
try {
const dividend = await analyzeDividend(data.code);
const dividendMd = formatDividendMarkdown(dividend);
analysisText += `\n\n${dividendMd}`;
} catch {}
}
return {
code: data.code, market: data.market, name,
verdict: extractVerdict(analysisText),
analysis: analysisText,
warnings, outputFormat,
generatedAt: new Date().toISOString(),
};
}
// ── 批量分析 ──────────────────────────────────────────────
export async function analyzeMultipleStocks(
stockDataList: StockData[],
outputFormat: OutputFormat = "markdown",
positions?: Record<string, PositionInfo>,
includeDividend = false
): Promise<AnalysisResult[]> {
const results: AnalysisResult[] = [];
for (const data of stockDataList) {
const position = positions?.[data.code];
results.push(await analyzeStock(data, outputFormat, position, includeDividend));
}
return results;
}
// ── K线图分析VLM──────────────────────────────────────
export async function analyzeChartImage(
imageUrlOrBase64: string,
stockCode: string,
isBase64 = false
): Promise<string> {
try {
const zai = await ZAI.create();
const imageContent = isBase64
? { type: "base64" as const, data: imageUrlOrBase64, mediaType: "image/png" as const }
: { type: "url" as const, url: imageUrlOrBase64 };
const completion = await zai.chat.completions.create({
messages: [
{ role: "system", content: "你是技术分析专家擅长K线形态识别。请用中文回答。" },
{
role: "user",
content: [
{ type: "image", image: imageContent },
{ type: "text", text: `这是 ${stockCode} 的K线图请分析\n1. 当前K线形态\n2. 趋势方向\n3. 关键支撑位和压力位\n4. 成交量配合\n5. 短期操作建议` },
],
},
],
});
return completion.choices[0]?.message?.content ?? "⚠️ VLM 未返回内容";
} catch (err: any) {
return `K线图分析失败${err.message}`;
}
}