/** * 买卖点标记的准备逻辑(纯函数,无 DOM/无图表实例依赖)。 * * 之所以单独成模块:Lightweight Charts 对标记有两条**硬约束**, * 违反时不会报错、只会静默丢标记或抛异常—— * 1. 标记的 `time` 必须存在于它所挂载的那条 series 的数据中; * 2. 标记数组必须按时间**升序**。 * 这两条属于「错了也看不出来」的坑,因此抽成纯函数并配单元测试 * (`components/charts/__tests__/markers.test.ts`,见 package.json 的 test:charts)。 */ import type { LwMarker } from "./LwChart"; /** * 判定「买入方向」。 * * 注意不能用 `kind.startsWith("BUY")`:`"SIG_BUY"` 并不以 `"BUY"` 开头, * 那样会把信号买点画成卖出箭头(aboveBar/arrowDown)—— 这正是本文件单元测试 * 捕获到的问题,因此这里显式枚举而不是做前缀匹配。 */ export function isBuyMarker(kind: LwMarker["kind"]): boolean { return kind === "BUY" || kind === "SIG_BUY"; } /** 后端语义(BUY/SELL)与图表语义(箭头位置/颜色)的映射在 LwChart 内完成,这里只做几何准备 */ export interface PreparedMarker { time: string; /** 原始语义,交给调用方决定颜色/形状 */ kind: LwMarker["kind"]; text: string; } /** * 过滤到真实存在的时间点、去重、按时间升序。 * * @param markers 页面给的语义标记(顺序任意、可能带有 series 里不存在的日期) * @param times 该 series 实际拥有的时间点(升序) */ export function prepareMarkers(markers: LwMarker[], times: string[]): PreparedMarker[] { const valid = new Set(times); const seen = new Set(); const out: PreparedMarker[] = []; for (const m of markers) { if (!valid.has(m.time)) continue; // 同一时间同一方向只保留一个(例如一天内多笔买入合并为一个买点) const key = `${m.time}|${m.kind}`; if (seen.has(key)) continue; seen.add(key); out.push({ time: m.time, kind: m.kind, text: m.text ?? m.label ?? (isBuyMarker(m.kind) ? "B" : "S"), }); } return out.sort((a, b) => (a.time < b.time ? -1 : a.time > b.time ? 1 : 0)); }