Files
Simon 9aaca12751 fix(web): 控件对齐与输入友好整改(控件高度令牌化 + 对齐自检脚本)
问题不是"不好看",而是**可测量的错位**:同一行里原生 date 输入 38.8px、数字输入
36.8px、按钮 34px;16px 的勾选框与 36.8px 的下拉同排;因子行的下拉与权重框没有
可见标签;列表里的勾选框点不中;参数非法时「运行回测」直接置灰且不说原因。

根因:控件高度靠「上下 padding + 行高」拼出来,而 input / select / button 的原生行高
各不相同(Chrome 的 date 还会多 2px),必然参差;加上各处内联像素宽度
(style={{width:220}}、flex:1)与自搓布局,列自然对不齐。

改动:
- 新增控件高度令牌 --ctl-h-sm/md/lg(28/34/38px)与 --ctl-px,.input/select/.btn/
  .icon-btn/date 统一显式 height(不再拼 padding);原生 checkbox/radio 统一 16px,
  点击热区交给外层 label(.check/.radio-row/.check--cell),表格整格可点
- 因子行、选股条件行改为「表头 + CSS 栅格」成列对齐,列宽由样式决定,
  去掉内联像素宽度与 flex 拉伸,配 aria-label 供读屏分辨重复行
- 表单友好化:错误提示改为**失焦或提交后**才出现(清空重填的瞬间不再标红);
  提交被拦下时一次展开全部行内错误 + 自动聚焦并滚动到第一个问题字段;
  运行/保存按钮不再因参数非法而置灰(灰按钮不说原因 = 看起来不可点却无响应),
  改为可点击并讲清原因;补齐 topN/costs 两处「产生了却没人显示」的行内错误落点
- 数值字段补 inputMode/step/min/max 与单位、取值范围提示;工具条检索/筛选用
  .input--search/.input--filter/.input--picker 类,不再写内联宽度
- /experiments 筛选无结果的空态与「暂无实验」区分开(原文案会让人以为归档丢了)
- 同一页面可能挂两份表单:radio name 与 label/for 加表单实例前缀(useId),
  否则两边单选互相取消、label 指错控件
- 新增 scripts/verify_ui_alignment.py:系统 Chrome + 原生 CDP(仅标准库,
  独占随机端口与临时 profile),按 7 个页面 × 1500/375px 检查同排等高、
  高度取值归一、点击目标、标签与无障碍名、字号圆角一致、尺寸匹配内容、
  横向溢出、提示裁切;本次基线 108/32 → 现 140/140

验证:pytest 388 passed、ruff 全绿(顺带清掉 qlib_verify.py 一处死代码)、
tsc 0 错误、图表单测 7 passed、next build 成功、契约自检 59/59、
对齐自检 140/140(含 375px 小屏)。
2026-09-27 09:01:55 +08:00

107 lines
4.3 KiB
Python
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.
"""Qlib(pyqlib git 源码安装)基线验证。
通过项(green,可重复):
1) qlib import 与版本
2) 合成数据 → calendars/day.txt、instruments/all.txt、features/<sym>/<field>.day.bin 落盘
3) qlib.init(provider_uri={"day": ...}) 后 calendars / instruments / feature 底层存储可直接读回
说明/受阻:
- 0.9.8.dev32 要求 feature 目录名与 instruments 第一列均为小写(如 600519.sh)
- instruments 文件为 3 列(instrument\\tstart\\tend),provider_uri 用 {"day": 路径} 字典形式
- D.features 高层通路需与官方 dump_bin 的 bin 对齐细节一致(当前手动 bin 读取会出现
形状不一致),完整「QLibDataset → Alpha158 → LightGBM → 回测」工作流属于
qlib_adapter.QlibEngine 的实现任务,本脚本仅验证「安装 + 数据落盘 + 底层读回」。
用法:cd backend && uv run python ../scripts/qlib_verify.py
"""
from __future__ import annotations
import shutil
import tempfile
from pathlib import Path
import numpy as np
import pandas as pd
N_DAYS = 60
SYMBOLS = ["600519.sh", "600036.sh"]
FIELDS = ["open", "high", "low", "close", "volume", "factor", "vwap"]
def main() -> None:
tmp = Path(tempfile.mkdtemp(prefix="qlib_verify_"))
try:
days = pd.bdate_range("2023-01-03", periods=N_DAYS)
# 1) calendars / instruments / features
(tmp / "calendars").mkdir(parents=True)
(tmp / "calendars/day.txt").write_text(
"\n".join(d.strftime("%Y-%m-%d") for d in days), encoding="utf-8"
)
(tmp / "instruments").mkdir(parents=True)
(tmp / "instruments/all.txt").write_text(
"\n".join(f"{s}\t{days[0].strftime('%Y-%m-%d')}\t{days[-1].strftime('%Y-%m-%d')}" for s in SYMBOLS),
encoding="utf-8",
)
# 合成行情用解析式生成(无随机项),不需要随机数发生器
for k, sym in enumerate(SYMBOLS):
(tmp / "features" / sym).mkdir(parents=True)
drift = 0.002 + 0.001 * k
price = np.cumprod(1 + drift + 0.005 * np.sin(np.arange(N_DAYS) * 0.9 + k))
close = 100.0 * price
open_ = np.roll(close, 1)
fmap = {
"open": open_,
"high": np.maximum(open_, close) * 1.005,
"low": np.minimum(open_, close) * 0.995,
"close": close,
"volume": 1e6 + np.arange(N_DAYS) * 1e3,
"factor": np.ones(N_DAYS),
"vwap": close,
}
for f in FIELDS:
(tmp / "features" / sym / f"{f}.day.bin").write_bytes(
np.asarray(fmap[f], dtype=np.float32).tobytes()
)
# 2) qlib 初始化与底层读回
import qlib
from qlib.config import REG_CN
qlib.init(provider_uri={"day": str(tmp)}, region=REG_CN)
from qlib.data.storage.file_storage import FileFeatureStorage, FileInstrumentStorage
cal = FileCalendarStorage_probe(tmp)
assert len(cal) == N_DAYS, f"日历读回 {len(cal)} != {N_DAYS}"
inst = FileInstrumentStorage("all", "day")
parsed = inst.data
assert set(parsed.keys()) == set(SYMBOLS), f"instruments 读回异常: {list(parsed)}"
for sym in SYMBOLS:
fs = FileFeatureStorage(sym, "close", "day")
n = len(fs.data)
assert n > 0, f"{sym} close 读回为空"
print(f" [OK] {sym}/close.day.bin 读回 {n} 个值")
# 3) qlib 组件可用性(Alpha158 / LightGBM)
import lightgbm # noqa: F401
from qlib.contrib.data.handler import Alpha158 # noqa: F401
from qlib.contrib.model.gbdt import LGBModel # noqa: F401
print("==== qlib 基线验证通过(安装/落盘/底层读回/组件导入)====")
print("提示:D.features→Dataset→模型→回测的高层工作流需在实现 QlibEngine 时")
print(" 按官方 dump_bin 的对齐规则落盘后再打通(见 docs/QLIB_VERIFICATION.md)。")
finally:
shutil.rmtree(tmp, ignore_errors=True)
def FileCalendarStorage_probe(uri: Path):
"""直接按文件读回日历(绕开 storage 构造签名差异)。"""
return (uri / "calendars" / "day.txt").read_text(encoding="utf-8").strip().splitlines()
if __name__ == "__main__":
main()