finance-dashboard/lib/watchlist.js

80 lines
2.6 KiB
JavaScript
Raw Permalink 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.

// 追蹤個股:分群清單(持久化於 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;
}