汇总三轮未提交的开发(每轮均在本机 MariaDB + 真实浏览器上验证):
1) 股息率案例(全市场股息率最高 n 只,默认 20,每 m 月择股)
- 新增日频估值表 daily_basic + 迁移;股息率因子(dv_ratio / dividend_yield / TTM)
- 名称历史表 stock_name_history:剔除 ST 按**择股日当时名称**判定,消除
「曾高股息后 ST」的股息陷阱(实测 3.70pp 偏差)
- 区间择股/调仓双周期(m 择股 / y 调仓)、指数成分与白名单、停牌近似剔除
- 复权因子口径核对(4,164,742 行、缺失 0.0%)、收盘价成交与涨跌停拦单
- 案例实测:2020-01-01~2026-09-04 总收益 +24.86%(年化 3.52%、回撤 -28.58%)
2) 策略库与前端统一
- strategy 表 + CRUD/PUT 原地更新 + `describe_strategy` 按 spec 真实推导
「一句话说明 + 计算公式 + 执行步骤 + 注意事项」(与引擎实执行规则同源)
- 任何出现股票代码处都成对显示名称且可点击进个股页
- 全站图表基座统一 TradingView Lightweight Charts(ECharts 依赖、
锁文件、组件与文档标注一并清除),买卖点标记只落在真实交易日上
3) 回测存档完整化(可往复查看)
- 同步端点(POST /api/backtests、/api/factor-tests)此前完全不落库 → 现在同样归档,
归档 id 经响应头 X-Experiment-Id 返回(不破坏 response_model)
- data_version 首次真实写入(数据快照指纹:最新交易日 + 各表规模)
- 个股收益曲线默认**全量保存**(此前硬截断 60 只);超出体积预算才裁剪,
并写 archive_meta(机器可读)+ unimplemented(人可读)如实标注
- 列表 kind/q 过滤 + X-Total-Count(此前 limit=50 静默截断)、DELETE 归档
- 只读归档页 /experiments/{id}(Server Component,SSR 直出**选股条件**与
**交易执行依据**);结果视图按 kind 分发(backtest/factor_test/selection),
非回测归档不套用回测口径
- 新增 CLI:prune_experiments(保留策略,默认 dry-run)、
restore_experiment_from_job(从 Job 副本按原 id 重建被删的历史归档,默认 dry-run)
门禁:pytest 388 passed、ruff All checks passed、tsc 0 错误、图表单测 7 passed、
next build 成功、契约脚本 verify_strategy_workspace 59/59(含按 kind 逐类验证归档页)。
273 lines
7.8 KiB
TypeScript
273 lines
7.8 KiB
TypeScript
/**
|
|
* 内联 SVG 图标集(Phosphor 风格线性图标,1.6px 描边,stroke=currentColor)。
|
|
* 不引入第三方图标库(离线可用),禁止用 emoji 作结构图标。
|
|
*
|
|
* 用法:<Icon name="search" size={16} /> 或 <Icon.Home .../>
|
|
*/
|
|
import type { ReactNode, SVGProps } from "react";
|
|
|
|
export type IconName =
|
|
| "logo"
|
|
| "grid"
|
|
| "candles"
|
|
| "flask"
|
|
| "layers"
|
|
| "gauge"
|
|
| "archive"
|
|
| "search"
|
|
| "x"
|
|
| "chevDown"
|
|
| "chevRight"
|
|
| "arrowUpRight"
|
|
| "arrowUp"
|
|
| "arrowDown"
|
|
| "play"
|
|
| "refresh"
|
|
| "spinner"
|
|
| "alert"
|
|
| "check"
|
|
| "info"
|
|
| "database"
|
|
| "calendar"
|
|
| "target"
|
|
| "chartLine"
|
|
| "scale"
|
|
| "book"
|
|
| "link"
|
|
| "spark"
|
|
| "clock"
|
|
| "filter"
|
|
| "plus"
|
|
| "edit"
|
|
| "trash"
|
|
| "download"
|
|
| "copy";
|
|
|
|
const P: Record<
|
|
IconName,
|
|
(p: { filled?: boolean }) => ReactNode
|
|
> = {
|
|
logo: () => (
|
|
<>
|
|
<path d="M4 20V4h4v6h4V4h4v10h4V4h4v16H4z" fill="currentColor" stroke="none" />
|
|
<path d="M3 20h18" strokeWidth="2.4" strokeLinecap="round" />
|
|
</>
|
|
),
|
|
grid: () => (
|
|
<>
|
|
<rect x="3.5" y="3.5" width="7" height="7" rx="1.6" />
|
|
<rect x="13.5" y="3.5" width="7" height="7" rx="1.6" />
|
|
<rect x="3.5" y="13.5" width="7" height="7" rx="1.6" />
|
|
<rect x="13.5" y="13.5" width="7" height="7" rx="1.6" />
|
|
</>
|
|
),
|
|
candles: () => (
|
|
<>
|
|
<path d="M7 3v3.2M7 17.8V21M7 6.2a1.8 1.8 0 0 1 1.8 1.8v6a1.8 1.8 0 0 1-1.8 1.8v0a1.8 1.8 0 0 1-1.8-1.8V8a1.8 1.8 0 0 1 1.8-1.8z" />
|
|
<path d="M17 3v2M17 19v2M17 5a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2v0a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z" />
|
|
</>
|
|
),
|
|
flask: () => (
|
|
<>
|
|
<path d="M9.5 3h5M10.5 3v6.2L5.2 18.6A2 2 0 0 0 6.9 21.5h10.2a2 2 0 0 0 1.7-2.9L13.5 9.2V3" />
|
|
<path d="M7.2 15.5h9.6" />
|
|
</>
|
|
),
|
|
layers: () => (
|
|
<>
|
|
<path d="M12 3.5 21 8.5l-9 5-9-5 9-5z" />
|
|
<path d="m4.5 12.5 7.5 4.2 7.5-4.2M4.5 16.5l7.5 4.2 7.5-4.2" />
|
|
</>
|
|
),
|
|
gauge: () => (
|
|
<>
|
|
<path d="M12 21a9 9 0 1 1 9-9" />
|
|
<path d="M12 12.5 16.6 6.8" />
|
|
<circle cx="12" cy="12.5" r="1.5" fill="currentColor" stroke="none" />
|
|
</>
|
|
),
|
|
archive: () => (
|
|
<>
|
|
<rect x="3.5" y="4" width="17" height="5" rx="1.6" />
|
|
<path d="M5.5 9v9.5a1.5 1.5 0 0 0 1.5 1.5h10a1.5 1.5 0 0 0 1.5-1.5V9" />
|
|
<path d="M10 13.5h4" />
|
|
</>
|
|
),
|
|
search: () => (
|
|
<>
|
|
<circle cx="11" cy="11" r="6.5" />
|
|
<path d="m20 20-4.4-4.4" strokeLinecap="round" />
|
|
</>
|
|
),
|
|
x: () => (
|
|
<>
|
|
<path d="m6 6 12 12M18 6 6 18" strokeLinecap="round" />
|
|
</>
|
|
),
|
|
chevDown: () => <path d="m6 9.5 6 6 6-6" strokeLinecap="round" strokeLinejoin="round" />,
|
|
chevRight: () => <path d="m9.5 6 6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />,
|
|
arrowUpRight: () => (
|
|
<>
|
|
<path d="M7 17 17 7M9 7h8v8" strokeLinecap="round" strokeLinejoin="round" />
|
|
</>
|
|
),
|
|
arrowUp: () => <path d="M12 19V5M5.5 11.5 12 5l6.5 6.5" strokeLinecap="round" strokeLinejoin="round" />,
|
|
arrowDown: () => <path d="M12 5v14M18.5 12.5 12 19l-6.5-6.5" strokeLinecap="round" strokeLinejoin="round" />,
|
|
play: ({ filled }) =>
|
|
filled ? (
|
|
<path d="M8 5.5v13a1 1 0 0 0 1.5.9l11-6.5a1 1 0 0 0 0-1.8l-11-6.5A1 1 0 0 0 8 5.5z" fill="currentColor" stroke="none" />
|
|
) : (
|
|
<path d="M8.5 5.8v12.4a.6.6 0 0 0 .9.5l10-6.2a.6.6 0 0 0 0-1L9.4 5.3a.6.6 0 0 0-.9.5z" fill="currentColor" stroke="none" />
|
|
),
|
|
refresh: () => (
|
|
<>
|
|
<path d="M20 12a8 8 0 1 1-2.3-5.6" strokeLinecap="round" />
|
|
<path d="M20 3.5V6.6a.4.4 0 0 1-.4.4H16.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
</>
|
|
),
|
|
spinner: () => (
|
|
<>
|
|
<path d="M12 3a9 9 0 1 0 9 9" strokeLinecap="round" />
|
|
</>
|
|
),
|
|
alert: ({ filled }) =>
|
|
filled ? (
|
|
<path
|
|
d="M12 2.6 23 20.8a1 1 0 0 1-.9 1.5H1.9a1 1 0 0 1-.9-1.5L12 2.6zM12 10v4.6m0 3.1v.1"
|
|
fill="currentColor"
|
|
stroke="none"
|
|
opacity="0.25"
|
|
/>
|
|
) : (
|
|
<>
|
|
<path d="M12 3.2 21.7 20a1 1 0 0 1-.85 1.5H3.15A1 1 0 0 1 2.3 20L12 3.2z" />
|
|
<path d="M12 9.5v4.4M12 17.4v.1" strokeLinecap="round" />
|
|
</>
|
|
),
|
|
check: () => <path d="m5 12.5 4.5 4.5L19 7.5" strokeLinecap="round" strokeLinejoin="round" />,
|
|
plus: () => <path d="M12 5.5v13M5.5 12h13" strokeLinecap="round" />,
|
|
edit: () => (
|
|
<>
|
|
<path d="M4.5 19.5h4l10-10a2.12 2.12 0 0 0-3-3l-10 10v3Z" strokeLinejoin="round" />
|
|
<path d="m14.5 6.5 3 3" strokeLinecap="round" />
|
|
</>
|
|
),
|
|
trash: () => (
|
|
<>
|
|
<path d="M5 7.5h14" strokeLinecap="round" />
|
|
<path d="M9.5 7.5V5.5h5v2" strokeLinejoin="round" />
|
|
<path d="M6.5 7.5 7.4 19a1.5 1.5 0 0 0 1.5 1.4h6.2a1.5 1.5 0 0 0 1.5-1.4l.9-11.5" strokeLinejoin="round" />
|
|
</>
|
|
),
|
|
download: () => (
|
|
<>
|
|
<path d="M12 3.5v10.5" strokeLinecap="round" />
|
|
<path d="M7.5 10 12 14.5 16.5 10" strokeLinecap="round" strokeLinejoin="round" />
|
|
<path d="M4.5 19.5h15" strokeLinecap="round" />
|
|
</>
|
|
),
|
|
copy: () => (
|
|
<>
|
|
<rect x="9" y="9" width="10.5" height="10.5" rx="2" />
|
|
<path d="M15 6.5A2 2 0 0 0 13 4.5H6.5a2 2 0 0 0-2 2V13a2 2 0 0 0 2 2" strokeLinecap="round" />
|
|
</>
|
|
),
|
|
info: () => (
|
|
<>
|
|
<circle cx="12" cy="12" r="9" />
|
|
<path d="M12 11v5.5M12 7.6v.1" strokeLinecap="round" />
|
|
</>
|
|
),
|
|
database: () => (
|
|
<>
|
|
<ellipse cx="12" cy="5.5" rx="7.5" ry="3" />
|
|
<path d="M4.5 5.5v6c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3v-6" />
|
|
<path d="M4.5 11.5v6c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3v-6" />
|
|
</>
|
|
),
|
|
calendar: () => (
|
|
<>
|
|
<rect x="3.5" y="5" width="17" height="15.5" rx="2" />
|
|
<path d="M8 3v4M16 3v4M3.5 10h17" />
|
|
</>
|
|
),
|
|
target: () => (
|
|
<>
|
|
<circle cx="12" cy="12" r="8.5" />
|
|
<circle cx="12" cy="12" r="4.5" />
|
|
<circle cx="12" cy="12" r="0.8" fill="currentColor" stroke="none" />
|
|
</>
|
|
),
|
|
chartLine: () => (
|
|
<>
|
|
<path d="M3.5 3.5v17h17" />
|
|
<path d="m6.5 15 4-5 3.5 3 4.5-6.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
</>
|
|
),
|
|
scale: () => (
|
|
<>
|
|
<path d="M12 4v16M8 20h8" />
|
|
<path d="m5 8 7-3 7 3-2.2 3H7.2L5 8z" />
|
|
<path d="M3.5 12.2 2 11l3-3M20.5 12.2 22 11l-3-3" strokeLinecap="round" strokeLinejoin="round" />
|
|
</>
|
|
),
|
|
book: () => (
|
|
<>
|
|
<path d="M4.5 5A2.5 2.5 0 0 1 7 2.5h12V18H7a2.5 2.5 0 0 0-2.5 2.5V5z" />
|
|
<path d="M4.5 20.5A2.5 2.5 0 0 1 7 18h12" />
|
|
<path d="M9.5 7h6" strokeLinecap="round" />
|
|
</>
|
|
),
|
|
link: () => (
|
|
<>
|
|
<path d="M10 14.5 14.5 10" strokeLinecap="round" />
|
|
<path d="M8.2 15.8 5.8 18.2a2.8 2.8 0 1 1-3.9-3.9l4-4a2.8 2.8 0 0 1 3.9 0" strokeLinecap="round" strokeLinejoin="round" />
|
|
<path d="M15.8 8.2l2.4-2.4a2.8 2.8 0 1 1 3.9 3.9l-4 4a2.8 2.8 0 0 1-3.9 0" strokeLinecap="round" strokeLinejoin="round" />
|
|
</>
|
|
),
|
|
spark: () => (
|
|
<>
|
|
<path d="M12 3.5 13.8 9l5.7 1.4-5.7 1.4L12 17.5l-1.8-5.7L4.5 10.4 10.2 9 12 3.5z" fill="currentColor" stroke="none" opacity="0.35" />
|
|
<path d="M19 15.5l.9 2.6 2.6.9-2.6.9-.9 2.6-.9-2.6-2.6-.9 2.6-.9.9-2.6z" fill="currentColor" stroke="none" />
|
|
</>
|
|
),
|
|
clock: () => (
|
|
<>
|
|
<circle cx="12" cy="12" r="8.5" />
|
|
<path d="M12 7.5V12l3 2" strokeLinecap="round" strokeLinejoin="round" />
|
|
</>
|
|
),
|
|
filter: () => (
|
|
<>
|
|
<path d="M4 6h16M7 12h10M10 18h4" strokeLinecap="round" />
|
|
</>
|
|
),
|
|
};
|
|
|
|
interface IconProps extends Omit<SVGProps<SVGSVGElement>, "name"> {
|
|
name: IconName;
|
|
size?: number;
|
|
/** 填充态(部分图标支持实心强调) */
|
|
filled?: boolean;
|
|
}
|
|
|
|
export function Icon({ name, size = 18, filled = false, ...rest }: IconProps) {
|
|
return (
|
|
<svg
|
|
className="ic"
|
|
width={size}
|
|
height={size}
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth={1.6}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
aria-hidden="true"
|
|
{...rest}
|
|
>
|
|
{P[name]({ filled })}
|
|
</svg>
|
|
);
|
|
}
|