feat: add ops-map text summaries for leverage and move periods
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
/** 作战地图图下文字结论(由 buckets 生成)。 */
|
||||
|
||||
export type LevBucket = {
|
||||
label: string;
|
||||
n: number;
|
||||
mean: number | null;
|
||||
pct_ge_min: number | null;
|
||||
};
|
||||
|
||||
export type MoveBucket = {
|
||||
label: string;
|
||||
n: number;
|
||||
mean_abs: number | null;
|
||||
mean_signed: number | null;
|
||||
};
|
||||
|
||||
const MIN_N = 3;
|
||||
const TOP_K = 5;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function enoughSamples<T extends { n: number }>(buckets: T[]): T[] {
|
||||
return buckets.filter((b) => b.n >= MIN_N);
|
||||
}
|
||||
|
||||
/** 上图杠杆结论 */
|
||||
export function leverageSummary(
|
||||
range: "day" | "week" | "month",
|
||||
buckets: LevBucket[],
|
||||
minLeverage: number
|
||||
): string {
|
||||
const active = withData(buckets);
|
||||
if (!active.length) {
|
||||
return "所选范围内暂无杠杆样本,采集后将显示各时段分布。";
|
||||
}
|
||||
|
||||
const bestMean = [...active].sort((a, b) => (b.mean ?? -1) - (a.mean ?? -1))[0];
|
||||
const minL = minLeverage;
|
||||
|
||||
if (range === "day") {
|
||||
const labels = active.map((b) => b.label).join("、");
|
||||
const parts: string[] = [];
|
||||
parts.push(`今日杠杆分布:有样本时段 ${labels}。`);
|
||||
if (bestMean?.mean != null) {
|
||||
const ge = bestMean.mean >= minL ? "已超过" : "未达到";
|
||||
parts.push(
|
||||
`${bestMean.label} 均值约 ${fmtNum(bestMean.mean)},${ge}达标线 ${minL}。`
|
||||
);
|
||||
}
|
||||
if (active.length <= 2) {
|
||||
parts.push(`采集初期,今日仅 ${active.length} 个时段有样本,结论仅供参考。`);
|
||||
}
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
const windowName = range === "week" ? "本周" : "本月";
|
||||
const ranked = enoughSamples(active)
|
||||
.filter((b) => b.pct_ge_min != null)
|
||||
.sort((a, b) => {
|
||||
const dp = (b.pct_ge_min ?? 0) - (a.pct_ge_min ?? 0);
|
||||
if (dp !== 0) return dp;
|
||||
const dn = b.n - a.n;
|
||||
if (dn !== 0) return dn;
|
||||
return (b.mean ?? 0) - (a.mean ?? 0);
|
||||
})
|
||||
.slice(0, TOP_K);
|
||||
|
||||
const parts: string[] = [];
|
||||
if (!ranked.length) {
|
||||
parts.push(
|
||||
`${windowName}各时段样本仍偏少(单时段不足 ${MIN_N} 条),暂不给出高概率名单;持续采集后会更稳定。`
|
||||
);
|
||||
} else {
|
||||
const list = ranked
|
||||
.map((b) => `${b.label}(${fmtPct(b.pct_ge_min ?? 0)})`)
|
||||
.join("、");
|
||||
parts.push(`${windowName}≥${minL} 出现概率较高的时段:${list}。`);
|
||||
}
|
||||
if (bestMean?.mean != null && bestMean.n >= MIN_N) {
|
||||
parts.push(`杠杆均值最高时段:${bestMean.label}(约 ${fmtNum(bestMean.mean)})。`);
|
||||
} else if (bestMean?.mean != null) {
|
||||
parts.push(
|
||||
`当前均值最高:${bestMean.label}(约 ${fmtNum(bestMean.mean)},样本 ${bestMean.n},偏少)。`
|
||||
);
|
||||
}
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
/** 下图波动结论 */
|
||||
export function moveSummary(
|
||||
range: "day" | "week" | "month",
|
||||
buckets: MoveBucket[],
|
||||
opts: {
|
||||
pendingExpiry?: boolean;
|
||||
pendingCount?: number;
|
||||
mode: "abs" | "signed";
|
||||
apiMessage?: string | null;
|
||||
}
|
||||
): string {
|
||||
if (opts.pendingExpiry) {
|
||||
const extra = opts.apiMessage ? ` ${opts.apiMessage}` : "";
|
||||
return `未到期,暂无法统计时段→到期波动;结算后将按日/周/月给出高波动钟点。${extra}`.trim();
|
||||
}
|
||||
|
||||
const active = withData(buckets).filter((b) =>
|
||||
opts.mode === "abs" ? b.mean_abs != null : b.mean_signed != null
|
||||
);
|
||||
if (!active.length) {
|
||||
return "所选范围内暂无已结算波动样本。";
|
||||
}
|
||||
|
||||
const windowName =
|
||||
range === "day" ? "今日" : range === "week" ? "本周" : "本月";
|
||||
|
||||
// 日视图样本少时放宽;周/月仍要求单时段样本足够
|
||||
const pool = range === "day" ? active : enoughSamples(active);
|
||||
const ranked = [...pool]
|
||||
.sort((a, b) => {
|
||||
if (opts.mode === "abs") {
|
||||
return Math.abs(b.mean_abs ?? 0) - Math.abs(a.mean_abs ?? 0);
|
||||
}
|
||||
return Math.abs(b.mean_signed ?? 0) - Math.abs(a.mean_signed ?? 0);
|
||||
})
|
||||
.slice(0, TOP_K);
|
||||
|
||||
if (!ranked.length) {
|
||||
return range === "day"
|
||||
? `${windowName}暂无可用波动样本。`
|
||||
: `${windowName}波动样本偏少(单时段不足 ${MIN_N} 条),暂不排名。`;
|
||||
}
|
||||
|
||||
if (opts.mode === "abs") {
|
||||
const list = ranked
|
||||
.map((b) => `${b.label}(${fmtNum(b.mean_abs ?? 0, 2)})`)
|
||||
.join("、");
|
||||
return `${windowName}波动较高时段:${list}。`;
|
||||
}
|
||||
|
||||
const list = ranked
|
||||
.map((b) => {
|
||||
const v = b.mean_signed ?? 0;
|
||||
const dir = v >= 0 ? "偏多" : "偏空";
|
||||
return `${b.label}(${fmtNum(v, 2)},${dir})`;
|
||||
})
|
||||
.join("、");
|
||||
return `${windowName}带符号波动较大时段:${list}。`;
|
||||
}
|
||||
Reference in New Issue
Block a user