/** 作战地图图下明细表(由 buckets 生成)。 */ export type LevBucket = { label: string; n: number; mean: number | null; pct_ge_min: number | null; days?: number; days_hit?: number; pct_days_hit?: number | null; }; export type MoveBucket = { label: string; n: number; mean_abs: number | null; mean_signed: number | null; days?: number; }; export type LevTableRow = { label: string; days: number; daysHit: number; pct: number | null; mean: number | null; n: number; }; export type MoveTableRow = { label: string; days: number; n: number; meanAbs: number | null; meanSigned: number | null; }; export type SummaryTable = | { kind: "empty"; message: string } | { kind: "table"; note?: string; rows: T[] }; function fmtPct(p: number): string { return `${Math.round(p * 100)}%`; } function fmtNum(n: number, d = 1): string { return n.toFixed(d); } function withData(buckets: T[]): T[] { return buckets.filter((b) => b.n > 0); } /** 达标概率优先用按日口径 pct_days_hit,兼容旧数据回退到样本口径。 */ function hitRate(b: LevBucket): number | null { if (b.pct_days_hit != null) return b.pct_days_hit; return b.pct_ge_min; } function dayCount(b: LevBucket): number { if (typeof b.days === "number") return b.days; return b.n > 0 ? 1 : 0; } function daysHitCount(b: LevBucket, minLeverage: number): number { if (typeof b.days_hit === "number") return b.days_hit; if (b.mean != null && b.mean >= minLeverage && b.n > 0) return 1; return 0; } /** 上图杠杆明细表 */ export function leverageSummaryTable( range: "day" | "week" | "month", buckets: LevBucket[], minLeverage: number ): SummaryTable { const active = withData(buckets); if (!active.length) { return { kind: "empty", message: "所选范围内暂无杠杆样本,采集后将显示各时段分布。", }; } const rows: LevTableRow[] = [...active] .map((b) => ({ label: b.label, days: dayCount(b), daysHit: daysHitCount(b, minLeverage), pct: hitRate(b), mean: b.mean, n: b.n, })) .sort((a, b) => { const dp = (b.pct ?? -1) - (a.pct ?? -1); if (dp !== 0) return dp; const dh = b.daysHit - a.daysHit; if (dh !== 0) return dh; return (b.mean ?? 0) - (a.mean ?? 0); }); const windowName = range === "day" ? "今日" : range === "week" ? "本周" : "本月"; const note = range === "day" ? `${windowName}各时段明细(达标 = 该时段均值 ≥ ${minLeverage})` : `${windowName}各时段明细(达标 = 当日该时段均值 ≥ ${minLeverage};概率 = 达标天数 ÷ 一共几天)`; return { kind: "table", note, rows }; } /** 下图波动明细表 */ export function moveSummaryTable( range: "day" | "week" | "month", buckets: MoveBucket[], opts: { pendingExpiry?: boolean; pendingCount?: number; mode: "abs" | "signed"; apiMessage?: string | null; } ): SummaryTable { if (opts.pendingExpiry) { const extra = opts.apiMessage ? ` ${opts.apiMessage}` : ""; return { kind: "empty", message: `未到期,暂无法统计时段→到期波动;结算后将按日/周/月给出各钟点明细。${extra}`.trim(), }; } const active = withData(buckets).filter((b) => opts.mode === "abs" ? b.mean_abs != null : b.mean_signed != null ); if (!active.length) { return { kind: "empty", message: "所选范围内暂无已结算波动样本。" }; } const windowName = range === "day" ? "今日" : range === "week" ? "本周" : "本月"; const rows: MoveTableRow[] = [...active] .map((b) => ({ label: b.label, days: typeof b.days === "number" ? b.days : b.n > 0 ? 1 : 0, n: b.n, meanAbs: b.mean_abs, meanSigned: b.mean_signed, })) .sort((a, b) => { if (opts.mode === "abs") { return Math.abs(b.meanAbs ?? 0) - Math.abs(a.meanAbs ?? 0); } return Math.abs(b.meanSigned ?? 0) - Math.abs(a.meanSigned ?? 0); }); const note = opts.mode === "abs" ? `${windowName}各时段→到期绝对波动明细` : `${windowName}各时段→到期带符号波动明细`; return { kind: "table", note, rows }; } /** @deprecated 保留短句接口给旧调用;新 UI 用表格。 */ export function leverageSummary( range: "day" | "week" | "month", buckets: LevBucket[], minLeverage: number ): string { const t = leverageSummaryTable(range, buckets, minLeverage); if (t.kind === "empty") return t.message; const top = t.rows.slice(0, 5); const list = top .map( (r) => `${r.label}(一共 ${r.days} 天,达标 ${r.daysHit} 天${ r.pct != null ? `,${fmtPct(r.pct)}` : "" })` ) .join("、"); return `${t.note}:${list}。`; } export function moveSummary( range: "day" | "week" | "month", buckets: MoveBucket[], opts: { pendingExpiry?: boolean; pendingCount?: number; mode: "abs" | "signed"; apiMessage?: string | null; } ): string { const t = moveSummaryTable(range, buckets, opts); if (t.kind === "empty") return t.message; const top = t.rows.slice(0, 5); if (opts.mode === "abs") { return `${t.note}:${top .map((r) => `${r.label}(${fmtNum(r.meanAbs ?? 0, 2)},${r.days} 天)`) .join("、")}。`; } return `${t.note}:${top .map((r) => { const v = r.meanSigned ?? 0; return `${r.label}(${fmtNum(v, 2)},${r.days} 天)`; }) .join("、")}。`; } export { fmtPct, fmtNum };