Files
market_intel/web/src/lib/opsSummary.ts
T
dekun 270e114658 feat: show ops-map summaries as detailed period tables
Replace paragraph text with day-hit stats (days/days_hit/pct) so leverage and move charts list each hour with counts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 17:12:23 +08:00

211 lines
5.5 KiB
TypeScript
Raw 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.
/** 作战地图图下明细表(由 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<T> =
| { 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<T extends { n: number }>(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<LevTableRow> {
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<MoveTableRow> {
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 };