Files
Simon f13b34c59e feat(web): 字段库页 + 界面单位换算 + 因子目录参数化 + 选股策略条件表单
字段库与单位:
- `/fields`:字段库管理页(中文名/说明可改、可停用;kind 是引擎事实不可改);
  单位只在字段自己的阶梯里选(总市值 = 万元/亿元),越界 422 原样展示。
- `lib/units.ts`:界面单位 ⇄ 基准单位换算集中一处,条件输入按界面单位回显、
  提交前换回基准单位(引擎只认基准单位,库里存的也永远是基准单位)。
- `SelectionStrategyForm` 取代 `StrategyParamsForm`:一个策略只定义「怎么选」
  (股票池 + 因子 + 过滤条件),条件字段来自字段库接口而不是前端硬编码枚举。

因子参数化(名字即身份,界面不许藏):
- `/factors` 新增「参数」列与展开行:精确引擎键、每个参数的允许范围、来源、依赖列
  (依赖列标明「引擎事实,不可改」);说明文案从「不可修改」改为
  「改参数 = 新建参数化因子 = 新身份,旧因子/既有策略不变义」。
- 「新建参数化因子」卡片:模板下拉 + 受控窗口(min/max)+ 方向枚举下拉,
  实时预览规范键/中文名/渲染公式;后端 422 的原文原样展示,不静默截断。
- 因子下拉显示中文名(含参数)与短键,**value 一律是完整引擎键**;
  停用的因子从各页候选里消失(/strategies、/selection、/signals、/factors/compose),
  既有策略/归档仍按名字解析;`/factors/compose` 顺手修了勾选框点击目标过小。
- `/fields` 加提示:字段库只列内置因子名,参数化因子在 /factors 管理,
  选股策略条件下拉里会一并出现。

tsc --noEmit 0 error;verify_ui_alignment 8 页 160 项全过(另跑 3 个选因子页 51/54)。
2026-10-01 16:38:29 +08:00

53 lines
2.6 KiB
TypeScript
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.
/**
* 单位换算(界面层)——「字段库单位」这件事唯一的一处实现。
*
* 背景(2026-10):字段库里的单位分两层,写错任何一层都会让策略静默算错:
*
* - **基准单位**(`ConditionField.base_unit`):引擎存储与比较用的单位,由数据源落库口径
* 决定(总市值=万元、成交额=元、成交量=股),**不可改**。策略 JSON、归档里的
* ConditionSpec、引擎求值全部只用基准单位 —— 所以归档永远复现得出来。
* - **界面单位**(`ConditionField.unit`):只在输入/显示这一层用的单位,用户可以在字段库里
* 从 `units` 给定的阶梯里选(万元 ⇄ 亿元)。提交前 ×factor,回显时 ÷factor。
*
* 这样「把总市值改成亿元」会立刻生效(输入 5 就是 5 亿元),但不会改动任何已存策略的
* 含义 —— 库里存的仍是 50000 万元。
*/
import type { ConditionField, UnitOption } from "@/lib/types";
/** 该字段当前界面单位 → 基准单位的换算系数(没有备选单位 → 1)。 */
export function unitScale(meta: ConditionField | undefined | null): number {
if (!meta?.units?.length || meta.units.length < 2) return 1;
return meta.units.find((u) => u.unit === meta.unit)?.factor ?? 1;
}
/** 基准单位值 → 界面单位值(除以系数)。 */
export function fromBase(v: number, scale: number): number {
return !scale || scale === 1 ? v : Number((v / scale).toPrecision(12));
}
/** 界面单位值 → 基准单位值(乘以系数)。 */
export function toBase(v: number, scale: number): number {
return !scale || scale === 1 ? v : Number((v * scale).toPrecision(12));
}
/** 基准单位(引擎口径);字段不在库里时为空。 */
export function baseUnitOf(meta: ConditionField | undefined | null): string {
return meta?.base_unit || meta?.unit || "";
}
/** 界面上该显示的单位后缀:只有真的能换算(有备选且当前不是基准)时才显示。 */
export function displayUnitOf(meta: ConditionField | undefined | null): string {
return unitScale(meta) === 1 ? "" : (meta?.unit ?? "");
}
/** 单位阶梯的说明文字,如「1 亿元 = 10000 万元」。 */
export function unitNote(meta: ConditionField | undefined | null): string {
const scale = unitScale(meta);
if (scale === 1) return "";
return `1 ${meta?.unit} = ${scale} ${baseUnitOf(meta)}(引擎按基准单位比较)`;
}
/** 某个单位在阶梯里的系数(找不到 → 1)。 */
export function factorOf(units: UnitOption[] | undefined, unit: string): number {
return units?.find((u) => u.unit === unit)?.factor ?? 1;
}