finance-dashboard/lib/watchlist.js

80 lines
2.6 KiB
JavaScript
Raw Permalink Normal View History

2026-06-04 09:32:28 +00:00
// 追蹤個股:分群清單(持久化於 SQLite KV與財報日曆 watchlist 分開)
import { getCachedEntry, putCachedJSON } from './db.js';
const STORE_KEY = 'stock:watchlist:v1';
const MAX_GROUPS = 24;
const MAX_SYMBOLS_PER_GROUP = 48;
const MAX_SYMBOLS_TOTAL = 200;
export const SYMBOL_RE = /^[A-Z0-9.\-]{1,12}$/;
function newGroupId() {
return `g_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
}
export function defaultWatchlist() {
return {
groups: [
{ id: 'default', name: '我的追蹤', symbols: [], order: 0 },
],
updatedAt: new Date().toISOString(),
};
}
function cleanSymbol(s) {
const sym = String(s || '').trim().toUpperCase();
return SYMBOL_RE.test(sym) ? sym : null;
}
function normalizeGroup(g, idx) {
const id = String(g?.id || '').trim() || newGroupId();
const name = String(g?.name || '').trim().slice(0, 40) || '未命名分群';
const symbols = [...new Set((g?.symbols || []).map(cleanSymbol).filter(Boolean))].slice(0, MAX_SYMBOLS_PER_GROUP);
return { id, name, symbols, order: Number.isFinite(g?.order) ? g.order : idx };
}
/** 驗證並正規化前端API 送來的完整結構 */
export function normalizeWatchlistPayload(raw) {
const base = defaultWatchlist();
if (!raw || typeof raw !== 'object') return base;
let groups = Array.isArray(raw.groups) ? raw.groups.map(normalizeGroup) : base.groups;
if (!groups.length) groups = base.groups;
groups = groups.slice(0, MAX_GROUPS).sort((a, b) => a.order - b.order || a.name.localeCompare(b.name, 'zh-Hant'));
const seenSym = new Set();
for (const g of groups) {
g.symbols = g.symbols.filter(sym => {
if (seenSym.has(sym) || seenSym.size >= MAX_SYMBOLS_TOTAL) return false;
seenSym.add(sym);
return true;
});
}
if (!groups.some(g => g.id === 'default')) {
groups.unshift({ id: 'default', name: '我的追蹤', symbols: [], order: -1 });
}
groups.forEach((g, i) => { g.order = i; });
return { groups, updatedAt: new Date().toISOString() };
}
export function getStockWatchlist() {
const row = getCachedEntry(STORE_KEY);
const val = row?.value;
if (!val?.groups) return defaultWatchlist();
return normalizeWatchlistPayload(val);
}
export function saveStockWatchlist(payload) {
const normalized = normalizeWatchlistPayload(payload);
putCachedJSON(STORE_KEY, normalized);
return normalized;
}
export function allWatchlistSymbols(data) {
const out = [];
const seen = new Set();
for (const g of data?.groups || []) {
for (const sym of g.symbols || []) {
if (!seen.has(sym)) { seen.add(sym); out.push(sym); }
}
}
return out;
}