Initial commit

This commit is contained in:
Z User
2026-06-06 05:21:10 +00:00
Unverified
commit 6664758a6d
493 changed files with 135653 additions and 0 deletions

View File

@@ -0,0 +1,292 @@
/**
* watchlist.ts
* 自选股管理 + 价格提醒(使用平台 storage 持久化)
* 移植原版三种提醒类型:目标价 / 止损价 / 信号变化
*/
import ZAI from "z-ai-web-dev-sdk";
import { WatchlistItem, WatchlistAlert, Market, Verdict } from "./types";
const STORAGE_KEY = "watchlist-data";
// ── Storage 封装 ──────────────────────────────────────────
async function loadWatchlist(): Promise<WatchlistItem[]> {
try {
const result = await (window as any).storage?.get(STORAGE_KEY);
if (result?.value) return JSON.parse(result.value) as WatchlistItem[];
} catch {}
return [];
}
async function saveWatchlist(items: WatchlistItem[]): Promise<void> {
try {
await (window as any).storage?.set(STORAGE_KEY, JSON.stringify(items));
} catch (err: any) {
console.error("[watchlist] 保存失败:", err.message);
}
}
// ── 获取当前价格 ──────────────────────────────────────────
async function fetchCurrentPrice(ticker: string): Promise<number | null> {
try {
const zai = await ZAI.create();
const completion = await zai.chat.completions.create({
messages: [{
role: "user",
content: `查询 ${ticker} 当前股价,只返回一个 JSON{"price": 数字},不要其他内容。`,
}],
thinking: { type: "disabled" },
});
const raw = completion.choices[0]?.message?.content ?? "{}";
const parsed = JSON.parse(raw.replace(/```json|```/g, "").trim());
return typeof parsed.price === "number" ? parsed.price : null;
} catch {
return null;
}
}
function detectMarket(code: string): Market {
if (/^\d{6}$/.test(code)) return "CN";
if (/^\d{4,5}\.HK$/i.test(code)) return "HK";
return "US";
}
// ── 添加自选股 ────────────────────────────────────────────
export async function addToWatchlist(
ticker: string,
opts: {
targetPrice?: number;
stopPrice?: number;
alertOnSignal?: boolean;
notes?: string;
} = {}
): Promise<{ success: boolean; action: string; message: string; item?: WatchlistItem }> {
ticker = ticker.toUpperCase();
const currentPrice = await fetchCurrentPrice(ticker);
if (currentPrice === null) {
return { success: false, action: "error", message: `无法获取 ${ticker} 的价格,请确认代码正确` };
}
const watchlist = await loadWatchlist();
const existingIdx = watchlist.findIndex((i) => i.ticker === ticker);
if (existingIdx >= 0) {
// 更新已有记录
const existing = watchlist[existingIdx];
if (opts.targetPrice !== undefined) existing.targetPrice = opts.targetPrice;
if (opts.stopPrice !== undefined) existing.stopPrice = opts.stopPrice;
if (opts.alertOnSignal !== undefined) existing.alertOnSignal = opts.alertOnSignal;
if (opts.notes !== undefined) existing.notes = opts.notes;
watchlist[existingIdx] = existing;
await saveWatchlist(watchlist);
return { success: true, action: "updated", message: `已更新 ${ticker} 的自选股设置`, item: existing };
}
// 新增
const item: WatchlistItem = {
ticker,
market: detectMarket(ticker),
addedAt: new Date().toISOString(),
priceAtAdd: currentPrice,
targetPrice: opts.targetPrice ?? null,
stopPrice: opts.stopPrice ?? null,
alertOnSignal: opts.alertOnSignal ?? false,
lastSignal: null,
lastCheck: null,
notes: opts.notes ?? null,
};
watchlist.push(item);
await saveWatchlist(watchlist);
const alertDesc = [
opts.targetPrice ? `目标价 $${opts.targetPrice}` : null,
opts.stopPrice ? `止损价 $${opts.stopPrice}` : null,
opts.alertOnSignal ? "信号变化提醒" : null,
].filter(Boolean).join("");
return {
success: true, action: "added",
message: `已添加 ${ticker} 到自选股(当前价 $${currentPrice}${alertDesc ? `,设置了:${alertDesc}` : ""}`,
item,
};
}
// ── 删除自选股 ────────────────────────────────────────────
export async function removeFromWatchlist(
ticker: string
): Promise<{ success: boolean; message: string }> {
ticker = ticker.toUpperCase();
const watchlist = await loadWatchlist();
const filtered = watchlist.filter((i) => i.ticker !== ticker);
if (filtered.length === watchlist.length) {
return { success: false, message: `${ticker} 不在自选股列表中` };
}
await saveWatchlist(filtered);
return { success: true, message: `已从自选股中删除 ${ticker}` };
}
// ── 查看自选股列表 ────────────────────────────────────────
export async function listWatchlist(): Promise<{
items: Array<WatchlistItem & {
currentPrice: number | null;
changePct: number | null;
toTargetPct: number | null;
toStopPct: number | null;
}>;
count: number;
}> {
const watchlist = await loadWatchlist();
if (!watchlist.length) return { items: [], count: 0 };
const items = await Promise.all(watchlist.map(async (item) => {
const currentPrice = await fetchCurrentPrice(item.ticker);
const changePct = currentPrice && item.priceAtAdd
? parseFloat((((currentPrice - item.priceAtAdd) / item.priceAtAdd) * 100).toFixed(2))
: null;
const toTargetPct = currentPrice && item.targetPrice
? parseFloat((((item.targetPrice - currentPrice) / currentPrice) * 100).toFixed(2))
: null;
const toStopPct = currentPrice && item.stopPrice
? parseFloat((((item.stopPrice - currentPrice) / currentPrice) * 100).toFixed(2))
: null;
return { ...item, currentPrice, changePct, toTargetPct, toStopPct };
}));
return { items, count: items.length };
}
// ── 检查提醒 ──────────────────────────────────────────────
export async function checkAlerts(
currentSignals?: Record<string, Verdict>
): Promise<{ alerts: WatchlistAlert[]; count: number }> {
const watchlist = await loadWatchlist();
const alerts: WatchlistAlert[] = [];
const now = new Date().toISOString();
for (const item of watchlist) {
const currentPrice = await fetchCurrentPrice(item.ticker);
if (currentPrice === null) continue;
// 目标价提醒
if (item.targetPrice && currentPrice >= item.targetPrice) {
alerts.push({
ticker: item.ticker,
alertType: "target_hit",
message: `🎯 ${item.ticker} 已触达目标价!当前 $${currentPrice.toFixed(2)} ≥ 目标 $${item.targetPrice}`,
currentPrice,
triggerValue: item.targetPrice,
timestamp: now,
});
}
// 止损价提醒
if (item.stopPrice && currentPrice <= item.stopPrice) {
alerts.push({
ticker: item.ticker,
alertType: "stop_hit",
message: `🛑 ${item.ticker} 已触达止损价!当前 $${currentPrice.toFixed(2)} ≤ 止损 $${item.stopPrice}`,
currentPrice,
triggerValue: item.stopPrice,
timestamp: now,
});
}
// 信号变化提醒
if (item.alertOnSignal && currentSignals?.[item.ticker]) {
const newSignal = currentSignals[item.ticker];
if (item.lastSignal && newSignal !== item.lastSignal) {
alerts.push({
ticker: item.ticker,
alertType: "signal_change",
message: `📊 ${item.ticker} 信号变化:${item.lastSignal}${newSignal}`,
currentPrice,
triggerValue: `${item.lastSignal}${newSignal}`,
timestamp: now,
});
}
// 更新最新信号
item.lastSignal = newSignal;
}
item.lastCheck = now;
}
// 保存更新后的 lastSignal 和 lastCheck
await saveWatchlist(watchlist);
return { alerts, count: alerts.length };
}
// ── 格式化自选股列表Markdown─────────────────────────
export function formatWatchlistMarkdown(data: Awaited<ReturnType<typeof listWatchlist>>): string {
if (data.count === 0) {
return `## 📋 自选股列表\n\n_自选股为空。使用 \`/stock_watch AAPL\` 添加股票。_\n`;
}
let md = `## 📋 自选股列表(共 ${data.count} 只)\n\n`;
md += `| 代码 | 当前价 | 较买入 | 目标价 | 止损价 | 距目标 | 最新信号 |\n`;
md += `|------|--------|--------|--------|--------|--------|----------|\n`;
for (const item of data.items) {
const price = item.currentPrice ? `$${item.currentPrice.toFixed(2)}` : "暂缺";
const change = item.changePct !== null
? `${item.changePct > 0 ? "🟢+" : "🔴"}${item.changePct.toFixed(2)}%`
: "—";
const target = item.targetPrice ? `$${item.targetPrice}` : "—";
const stop = item.stopPrice ? `$${item.stopPrice}` : "—";
const toTarget = item.toTargetPct !== null ? `${item.toTargetPct > 0 ? "+" : ""}${item.toTargetPct.toFixed(1)}%` : "—";
const signal = item.lastSignal ?? "—";
md += `| ${item.ticker} | ${price} | ${change} | ${target} | ${stop} | ${toTarget} | ${signal} |\n`;
}
// 已触发的提醒
const triggered = data.items.filter(
(i) => (i.targetPrice && i.currentPrice && i.currentPrice >= i.targetPrice) ||
(i.stopPrice && i.currentPrice && i.currentPrice <= i.stopPrice)
);
if (triggered.length > 0) {
md += `\n### ⚡ 已触发提醒\n`;
for (const item of triggered) {
if (item.targetPrice && item.currentPrice && item.currentPrice >= item.targetPrice) {
md += `- 🎯 **${item.ticker}** 已达目标价 $${item.targetPrice}\n`;
}
if (item.stopPrice && item.currentPrice && item.currentPrice <= item.stopPrice) {
md += `- 🛑 **${item.ticker}** 已触止损 $${item.stopPrice}\n`;
}
}
}
return md;
}
// ── 格式化提醒结果Markdown────────────────────────────
export function formatAlertsMarkdown(result: Awaited<ReturnType<typeof checkAlerts>>): string {
if (result.count === 0) {
return `## 🔔 自选股提醒检查\n\n_当前没有触发任何提醒。_\n`;
}
let md = `## 🔔 自选股提醒(${result.count} 条触发)\n\n`;
for (const alert of result.alerts) {
md += `- ${alert.message}\n`;
}
return md;
}