Commit Graph
54 Commits
Author SHA1 Message Date
wshy 3db7f38446 Merge pull request #281 from 0112020179/codex/fix-enriched-refresh-memory
fix(oom): 降低补算内存峰值并统一重任务并发控制
2026-09-09 11:36:22 +08:00
0112020179 771a95a6d1 fix(oom): 降低补算内存峰值并统一重任务并发控制 2026-09-09 11:09:48 +08:00
kevin9327 7a13713903 fix(fundamentals): 换报告期公告当日不再丢失上一期财务因子值
attach_fundamental_factors 用公告日做 asof 回看再按「严格大于」门控:
日期正好等于新一期公告日时, join_asof 已匹配到「当天还不能用」的新记录,
门控随即置 null, 打断上一期的前向填充。矩阵路径 build_fundamental_matrices
按 searchsorted(side="right") 逐期覆盖, 公告当日保留上一期, 两条路径因此不一致
(其 docstring 声明与 attach_fundamental_factors 同口径)。

asof 键改用生效日 (公告日次日), 只命中已生效的报告期; 公告前仍为 null 的门控不变。
补 tests/test_fundamental_factors.py 的两条公告一致性用例。
2026-09-09 07:19:09 +09:00
shy3130 58b161b16d fix(enriched): 自愈孤儿 publishing 标记, 步进优化不再误报 publishing
清库半删除等异常会留下 owner 已死但状态仍为 publishing 的 generation
标记, 后续读取直接抛 EnrichedGenerationUnavailableError, 步进优化连锁失败。

- 读取侧仅在「owner 是其他进程且该进程已死」时判孤儿并在独占锁内
  自愈 (换新 uuid 重建 ready 标记); 同 pid 无活跃对象时保守不判,
  避免暴露清库半删除的数据 (fail-closed)
- 回测引擎改为 data_generation_await 轮询等待 (300s 上限), 遇短暂
  publishing 自动重试而非直接失败; worker 错误文案同步为可重试提示
2026-09-07 15:25:27 +08:00
shy3130 e3a2fbce13 feat(ext-data): 扩展表字段接入信号与因子(数值因子/评分 + string 归属筛选)
数值字段(int/float) → 信号+因子双通道:
- ext_factors: 帧组装时 join 扩展列并注册 kind=base 因子(分组「扩展数据」),
  时序模式按 (symbol,交易日) 精确对齐无未来函数, 快照模式仅当日单日帧
  注入(历史帧跳过防未来函数)
- registry.all_factors 惰性同步(配置目录签名幂等, 以注册表为权威增删);
  custom_signals.allowed_fields 自动并入 → 信号下拉/因子库/AI提示词/检验
  同一份清单; factor 补算入口按需注入
- 失效链: 上传/拉取/配置变更自动清扩展帧缓存+策略缓存, API层补
  repo.clear_cache; 写入后下一次计算立即生效
- 列名保留中文(预设表字段名), 非ASCII数值字段只进信号不注册因子
  (DSL标识符ASCII-only)

string 字段(概念/行业归属) → 仅信号条件通道:
- 运算符 包含(contains,字面量匹配非正则)/等于/不等于, 右值为字符串字面量,
  可与数值条件混合(强势板块归属 AND 热度阈值)
- 前端信号编辑器按字段类型切换运算符与右值输入; /options 暴露
  stringFields; AI 提示词含字符串字段清单与 contains 用法
- string 不注册为因子(数值口径), 空值不误报

测试: test_ext_factors 18个(PIT对齐/跨日不泄露/快照门控/写入失效/
contains字面量/中文列名端到端等); 存量因子计数测试补 data/ 运行时隔离
夹具(黄金断言不依赖本机扩展表); 受影响回归148个全过; pnpm build 通过;
ruff 对齐 main 基线
2026-09-06 18:27:33 +08:00
shy3130 c289ac5767 fix(backtest): 步进优化支持 python_history_legacy/polars_expr 策略
问题: WalkForwardService._prepare_shared_matrix 对非 matrix_native 策略直接抛
"步进优化暂仅支持矩阵策略", filter_history 类自定义策略 (如 everbloom 系列)
无法做步进优化。

根因: 闸门把非矩阵后端一刀切, 但 run() 内部本就存在 shared_market_data=None
的通用路径 (每折独立优化 + 独立 OOS 回测), 只是永远不会被走到。

方案: 闸门改分流 —— matrix_native 仍走共享矩阵加速路径; python_history_legacy
与 polars_expr 返回 None 走通用每折回测路径 (正确但无矩阵加速); composite /
minute_filter 维持 fail-closed 拒绝, 错误文案同步更新。

兼容: matrix_native 行为不变; 结果字段 shared_market_data=false 标识通用路径。
性能: 非矩阵策略每组合×每折一次完整回测, 大网格耗时线性放大 (UI 已有耗时提示)。

验证: uv run --frozen pytest tests/backtest/test_walkforward.py -q (23 passed,
含新增 3 例: legacy 走通用路径/polars_expr 放行/minute_filter 拒绝);
tests/backtest 全套 282 passed; ruff 无新增告警;
真实 API 端到端: everbloom_tupengpan 2x2 网格 120/30 步进 8/8 折全部完成。
2026-09-05 20:33:31 +08:00
shy3130 e0cd625ef4 feat(platform): 因子平台与因子↔策略双向联动 v0.2.3
- 因子平台: /factors 一级页(检验/因子库/编辑器/组合/挖掘), DSL 公式因子(25 算子点选、双语字段、我的因子模板、脏公式守卫), 版本与生命周期, 自动挖掘 L1 统计筛选
- 因子↔策略四条桥: 触发器 Zap 快建因子条件信号、因子一键生成排名策略、自定义信号 AI 提示词接入因子分组、策略回测因子归因(胜/败单入场信号日因子均值, 独立 tab, 双语因子名)
- 回测: 统计卡新增盈亏比(≥1 红/<1 绿), 蒙卡回撤合并为中位/95% 双值卡(自适应字号), 高级设置基础过滤与策略编辑器参数对齐(5 组区间)
- 信号库独立页 /signals(原设置 tab 迁出), 持仓提醒入导航; 挖掘并入因子页第 5 tab, /mining 旧链接重定向
- 研究线配套: 因子目录 61→77(评分/矩阵双内核), stats_v2(Newey-West/BH-FDR/DSR), enriched 管道与异动/报价服务配套调整
- 文档: README 导航与特性表、features.md 因子平台章节、操作说明书 9.2、factor-platform-plan 执行状态与 §5、二开文档桥接说明; 交流与支持节改版
- 版本 0.2.2 → 0.2.3; 后端全量 1625 passed(1 例环境性跳过), 前端 build 通过
2026-09-05 15:41:15 +08:00
shy3130 e89ea9becf fix: 修复 4 个 P1 issue (#224/#232/#223/#215)
- #224 screener 自定义 SQL 的内存连接关闭 enable_external_access,
  注入的 read_parquet/COPY 文件读写直接报错 (安全)
- #232 指数展示缓存百分数口径在消费边界显式 /100:
  pipeline._bench_rt_pct_of 与 abnormal_moves._bench_rt_pct 两处,
  修复 3/10/30 日偏离值被放大两个数量级
- #223 盘后管道按同日 daily/enriched 行数比较检测实时合并提前
  创建的部分分区, 删除后由增量重算全市场补齐
- #215 _basic_filter_for_asset 扩展中和股票专属键 (price_min/max/boards),
  并应用到回测/挖掘/策略扫描三个运行期入口, 修复 ETF 静默零信号
2026-09-03 13:01:43 +08:00
shy3130 41205b197c chore: follow-ups for PR #231/#233 reviews
- monitor_rules: raise from None in remind_date validation (B904)
- lots: log warning on asset-type fallback to stock (fail-open traceability)
- matrix: demote turnover NaN-column notice to debug
2026-09-03 12:45:24 +08:00
SeerGlaucus ba83e0241b fix(backtest): ETF矩阵回测不因缺失换手率字段而失败
非股票资产(etf/index)无股本数据(维表仅 symbol/name/code/asset_type),
数据源也不提供换手率; 但矩阵缓存档 common_filter 强制 turnover_min:0.0,
依赖解析仍为 ETF 请求 turnover_rate(需 float_shares 派生), 导致矩阵缓存
构建失败。实测内置 low_volatility_leader 也报
"matrix turnover_rate requires float_shares"。

- _basic_filter_for_asset 将 turnover_min/max 一并中和(与市值界同族, 上次修复遗漏)
- _populate_matrix_derived_arrays 对无股本的 etf/index 把缺失的 turnover_rate
  降级为全 NaN 列(与运行期 _optional_field 语义一致), 股票派生路径行为不变
- 新增回归测试: ETF 无股本时降级 NaN 不报错; 有 float_shares 时仍正常派生
2026-09-03 02:37:52 +08:00
shy3130 1a7e6ad32c Merge remote-tracking branch 'origin/main' into feat/capability-routing
# Conflicts:
#	README.md
#	backend/app/backtest/worker.py
2026-08-30 19:47:11 +08:00
shy3130 b8fa08f027 fix(worker): 消除回测子进程结果消息丢失竞态
现象: 整年区间回测偶发 'backtest worker exited without result (exitcode=0)'。
根因: 子进程 event_queue.put 只是把消息交给后台 feeder 线程, 主线程随即退出,
feeder 随进程销毁, 大结果/高负载下消息尾部未刷入管道; 父进程 0.1s 轮询
恰在 Empty+进程已死时跳出循环, 未读到的 result 被丢弃。

修复 (双侧):
- 子进程 finally 中 close+join_thread 队列, 保证退出前消息完整刷入管道
- 父进程在判定无结果前做一次兜底排空 (join 后 1s×2 轮 get)
2026-08-30 19:05:19 +08:00
shy3130 0559632de6 feat(backtest): 分钟策略回测 v1 — 逐交易日回放信号分钟收盘入场
- 新增 MinuteSignalReplayer: 枚举区间内分钟分区日, 逐日组装 StrategyDataContext
  (timeframe=1m, 日线窗口严格止于 T-1) 并复用 strategy_engine.run, 与实盘选股同路径
- 缺分区日显式跳过并记入 skipped_days, 不回退最近分区
- 涨停拒买: 信号分钟收盘 >= 当日涨停价(T-1 raw_close + 板块幅度)剔除并计数
- MarketMatrix 新增 entry_price 覆盖矩阵, 引擎在有限值处优先于 open/close 惯例
- repository.list_minute_dates 按目录名枚举分钟分区日, 零 parquet 扫描
- strategy.run 增加 minute_filter 分支: 入场 delay 0/离场沿用日K matcher,
  trades.entry_date 补全为 YYYY-MM-DD HH:MM (北京时间)
2026-08-30 19:05:16 +08:00
shy3130 afbf432eae fix(backtest): 子进程退出收尾超时不再丢弃已送达的回测结果
终态消息入队后显式冲刷队列并以 os._exit 立即退出, 跳过大数据量下
可达数十秒的解释器 teardown (GC/DuckDB 线程 join/DLL 卸载); 父进程
在子进程超时未退出时改为强杀并采纳已送达结果, 记录 worker_exit_forcibly
指标, 错误场景优先抛出 worker 真实异常。
2026-08-26 22:07:40 +08:00
shy3130 55b8e739c3 fix(mining): enriched 数据并发更新时重读快照而非整轮失败
用户在数据更新进行中跑挖掘: 面板读取期间 enriched 发布提交新世代,
assert_data_generation 直接抛 EnrichedGenerationUnavailableError,
整轮运行作废且只留一句英文堆栈; 启动接口撞上发布中状态还会 500。

- mining_runtime: 面板+撮合矩阵读取包进有限重试环 (默认 3 次)。
  世代变化时丢弃半新半旧的读取, 用新世代整体重读; 发布未完成时先等
  5s; 耗尽后以带指引的中文错误终止。排队指纹仍只在首轮校验, 重试跟随
  新世代等价于"更新后立刻重跑"
- api/mining: build_data_fingerprint 撞上发布中状态映射为 400 中文
  提示 (原来未捕获直接 500), 前端 task.error 红字通路直接可见
- 测试: 读取中单次世代漂移重读后成功 / 持续漂移耗尽后带指引失败 /
  发布中启动返回 400; 全量挖掘回归 103 passed
2026-08-26 13:19:16 +08:00
shy3130 de51929a44 fix(mining): 市场环境数据缺失时挖掘启动即失败并前置提示
用户在未计算市场环境的数据目录跑挖掘: 搜索阶段的 regime 评估被
evaluator 软捕获成 error 字符串, 整轮嵌套搜索照常跑完后, artifacts
阶段的 _regime_date_count 硬抛 ValueError, 计算全部浪费且只在最后
给出指引。

- mining_runtime: require_regime 时在因子面板就绪后立即用与
  artifacts 相同的 fold 口径预构建一次 regime mask (required 区间取
  全部外层 fold 测试窗并集), 数据为空/覆盖不完整即刻带指引消息终止
- MiningWorkbench: 启动前查询 /api/regime/latest, 未计算时侧栏显示
  警告横幅(直达数据页)并在开始时 toast 阻止, 避免产生必败的运行记录
- 测试: 缺数据 fail-fast / 覆盖充足通过 / fold 窗口内缺口报错 三例
2026-08-26 12:55:38 +08:00
shy3130 61130cfdff fix(backtest): ETF/指数矩阵链路不再要求股本字段, 修复因子挖掘准备阶段失败
etf 维表(index_sync)物理上只有 symbol/name/code/asset_type, enriched 窄表
也不落盘股本字段; 而 DEFAULT_BASIC_FILTER 与矩阵缓存档 common_filter 均带
非 None 市值界, 依赖解析会无条件要求 total_shares/float_shares, 导致
_resolve_matrix_storage_fields 抛
"matrix parquet fields unavailable: ['float_shares', 'total_shares']"
(用户反馈: ETF 因子挖掘死于「准备共享撮合矩阵」阶段)。

- 新增 _basic_filter_for_asset: 非股票资产将市值/流通市值四项界置 None,
  依赖解析前中和; 运行期过滤本就对缺失股本列降级 no-op, 无需改动
- StrategyDependencyResolver.resolve 新增 asset_type 参数(默认 stock,
  旧调用行为不变), 四个调用点传入真实资产类型:
  build_matrix_cache_profile / prepare_matrix_optimization /
  单次回测 / composite 子计划
- mining_runtime._prepare_base_market 两处 resolve 传入 request.asset_type
- 新增回归测试: ETF 计划与缓存档不含股本字段、股票行为不变、
  ETF 维表下字段解析成功、反向对照锁定失败模式

验证: 全量 pytest 976 passed; 真实组件端到端复现用户失败路径已转绿
(ETF enriched + 四列维表, 共享矩阵 (280,3) 构建成功)。
2026-08-23 17:46:54 +08:00
shy3130 0f2c7ce9b0 fix(backtest): 因子环境统计容忍数据边界=正式首日, 全部/1年范围不再报错
数据边界即正式首日 (本地数据首日=回测起点, 如「全部」/「1年」) 时, 首日
无 T-1 环境属正常。策略回测已有 clamp_formal_start_for_regime 首日让渡,
因子回测的环境分组统计路径漏了同样容差, 会整体报错阻断回测。

- align_regime_t_minus_one 新增 first_day_boundary_ok 参数 (默认 False,
  过滤场景 fail-closed 行为不变): 统计场景首日无前驱时跳过首日不参与分组
- 因子 _calc_regime_stats 传入容差, 与策略侧首日让渡同口径;
  内部缺口 (次日 T-1 缺环境) 仍 fail-closed 报错
- 回归测试: 边界容忍 + 分桶数正确 / 容差仅放行首日 / 内部缺口仍拒绝
2026-08-22 20:57:40 +08:00
shy3130 f856b4a8f8 回测「全部」范围与环境过滤冲突时自动顺延正式起点
正式首日 = 面板首日 (数据边界, 如「全部」范围从本地数据第一天开始) 时,
T-1 环境校验所需的上一交易日不在面板内, fail-closed 直接报错且用户
无法补更早数据。新增 clamp_formal_start_for_regime: 首日让渡为预热,
正式起点顺延到第二个交易日 (首日环境即成为次日 T-1), 仅损失 1 个
正式交易日。接入 run() 三条数据分支与优化器共享矩阵路径, 结果 config
回显实际生效起点。
2026-08-21 17:41:42 +08:00
shy3130 697c27bb02 feat(v0.2): 市场阶段与主线识别 + 因子挖掘全链路 + 数据层完善
- 市场环境: 新增情绪周期6阶段(冰点/启动/主升/高潮/退潮/修复, 连板梯队驱动,
  EMA平滑+2日确认+弱档否决, 平均段长9.7天)与概念/行业主线排名(涨停梯队聚合,
  可配置宽基/风格标签过滤); 市场环境页重构, regime 透明加列, 与5档state并存
- 挖掘: 因子与策略挖掘全链路(API/worker/进程锁/候选库/前端工作台/文档),
  周度调度默认关闭且永不自动发布
- 回测: 财务快照因子(点时口径), 批量回测预计算共享下期收益,
  信号路径矩阵列依赖展开修复(consecutive_limit_ups 缺列报错)
- 数据/性能: enriched 生成与预热治理, 重任务限流, 行情/K线缓存复用, 时区修复
- 测试: 后端全量 914 通过; GUI 黑盒验证截图存证 gui-test-screenshots/
2026-08-16 23:39:07 +08:00
shy3130 a0cb899163 perf(backtest): 因子批量回测预计算共享下期收益并向量化统计
P1 — run_batch 进入因子循环前对 close 有效序列计算一次 _next_return
(仅依赖 symbol/date/close/rebalance, 与因子无关), 通过 left join 附着到
共享面板; _evaluate_panel 检测到预计算列时直接复用, 跳过逐因子 shift/
调仓日 JOIN。因子空值集中在预热期前缀, 过滤后无内部空洞, 故与逐因子
在过滤后面板上重算的结果完全等价 (daily/weekly/monthly 均数值一致)。

P2 — 将 _calc_group_nav / _calc_group_stats / _calc_long_short 三段
逐行 Python 循环改为向量化:
- 分组净值: Polars cum_prod 替代 dict 累乘
- 分组统计: numpy maximum.accumulate 计算回撤, 向量化周期收益序列
- 多空组合: cumprod 合成净值, 向量化峰值/回撤
峰值初值沿用原逻辑 max(1.0, 历史最高), 400 组随机序列 (含 null/负收益)
下与旧实现逐值等价。

测试: tests/backtest 全量 184 passed。
2026-08-14 13:24:24 +08:00
shy3130 8fe5dac7bd feat(v0.2): 完善量化研究与二次开发能力 2026-08-13 20:16:02 +08:00
shy3130 57eb641275 fix(backtest): 修复市场环境过滤未生效 2026-08-11 12:43:46 +08:00
shy3130 4ee55e40c6 feat(regime): 新增市场状态识别系统并接入叠加策略回测过滤器
后端
- 新增 services/regime_builder.py:5 档日级市场状态分级(strong/lean_strong/range/lean_weak/weak)
- 新增 api/regime.py:5 个 regime 查询接口
- daily_pipeline 接入 regime 构建(step 2.6,soft-fail + 双检测自愈)
- 回测 strategy.py 新增 regime_filter 过滤:采用 T-1 防未来函数,仅作用于入场信号
  在 composite / matrix_native / prepared 三处注入 entry_time_mask
- backtest API 透传 regime_filter(REST + SSE);_make_job_key 纳入 regime_filter 以隔离缓存
- 兼容 _RepoStub 测试夹具(store 属性缺失场景)

前端
- 新增 Regime 页面、路由、导航入口、api 类型与 queryKeys
- 叠加策略回测页新增 regime 过滤器控件

测试
- 新增 tests/test_regime_builder.py(18 项),覆盖分级逻辑、T-1 防未来函数、空值降级、三处 mask 注入
- 全量后端测试 582 passed;前端 pnpm build 通过;git diff --check 无空白错误
2026-08-02 12:05:20 +08:00
shy3130 4696fef959 feat(composite): 新增叠加策略(composite)支持选股与回测
引入第四种执行后端 composite: 声明式引用多个子策略, 复用现有注册表/缓存/撮合全链路。

核心设计:
- 退出采用来源投影(每个子策略 exit 仅在自己持仓窗口生效, 不串平其他子策略仓位)
- 评分用标准化排名加权(跨子策略可比, 不依赖 per-strategy 的 min-max 量纲)
- 来源归因(entry_signal_code 标记来源子策略)

后端:
- engine.py: CompositeSpec 数据模型, 两阶段引用校验(孤儿移除/嵌套禁止/asset_types 子集/≤8 上限), _run_composite_strategy 选股执行, find_dependents 删除防护, override_loader 注入保证子策略与单独跑同口径
- composite.py(新): 选股 merge_results + 回测 merge_signal_matrices 合并器(退出投影/排名融合/归因)
- backtest/strategy.py: composite 回测分支(特征计划合并/逐子信号/统一风控/basic_filter/归因)
- api/strategy.py: POST /composite/save 端点, _strategy_detail 返回 composite_children(含 name/source), 删除依赖 409 防护
- monitor.py: 实时监控 fail-closed(回退盘后缓存)
- walkforward.py: 对 composite 显式中文报错
- worker.py: _strategy_dirs 补 composite 目录(修复回测子进程找不到 composite 的 bug)

前端:
- CompositeStrategyDialog(新): 创建/编辑弹窗(自动生成 composite_ ID, 权重归一, 类型标签, 增删子策略)
- StrategySettingsDialog: composite 设置面板(子策略增删改/归一/类型标签)
- StrategyBacktest: 回测页 composite 适配(分组/参数区/结果区展示子策略构成)
- api.ts: 类型扩展(source/backend 加 composite, composite_children 含 name/source)

测试: 36 个 composite 专项测试(加载/选股/回测退出投影/排名/归因/override 透传/API/删除防护), 端到端验证真实内置策略回测通过
2026-08-02 10:56:03 +08:00
CJohn 33921fcc51 fix: 串行化 Numba parallel 内核,避免策略页并发崩溃
并发 run_all 会触发 workqueue Concurrent access,导致后端进程中断。
为 matrix parallel 内核加进程锁,前端对 run_all 做 pending 去重。
2026-07-26 11:56:10 +08:00
shy3130 60fe9e6fa6 修复回测指标数据缺失提示问题 2026-07-22 13:30:02 +08:00
shy3130 8e4258cf20 feat: support historical price limits and share capital 2026-07-18 23:37:54 +08:00
shy3130 44ab51e65d feat: improve strategy scoring and backtest execution 2026-07-18 16:59:53 +08:00
shy3130 34eaba3010 fix: handle uncovered walk-forward folds 2026-07-17 14:00:16 +08:00
shy3130 c763a6970f fix: restore strategy and data compatibility 2026-07-16 14:34:07 +08:00
lytem28 b6cf0495e1 feat: complete matrix-native backtest engine
Unify strategy execution across backtest, screener, and monitoring; isolate backtest workloads in spawn workers; and add shared matrix caching plus valid-bar indicator acceleration.
2026-07-16 12:17:27 +08:00
wshy b869f3a552 Merge pull request #104 from im47cn/feat/wf-optimization
feat(walkforward): 滚动窗口 walk-forward 优化 — IS 优化 + OOS 验证
2026-07-13 18:48:47 +08:00
im47cn e4262d9951 fix(walkforward): IS 训练折强制 position 模式, 堵前视泄漏
对抗式审查发现: WF UI 暴露 full 模式, 选中时训练折(end=train_end)未平仓的持仓
会用 train_end 之后(即 OOS 区间)的真实 K 线平仓 -> IS 分数被未来数据污染, 优化选出
的最优参数乐观偏移, 使过拟合被掩盖 (对专门检测过拟合的 WF 工具危害尤重)。

修复: IS 训练区间优化强制 mode="position" (只看正式区间表现), OOS 回测保留用户
所选 mode。新增测试: 用户选 full 时断言每折 IS 得到 position、OOS 保留 full。
2026-07-12 21:48:13 +08:00
shy3130 97e1cdcc78 feat(backtest): 交易记录显示具体触发信号 (不再只显示「信号」类别)
原问题: 多个信号在 _build_signal_mask 里 OR 成一个 mask 后, 只记录
exit_reason="信号", 具体哪个信号触发的信息丢失。回测里买入/卖出原因
只能看到笼统的「信号」, 不知道是 MA金叉还是 MACD死叉。

实现:
- 后端 TradeRecord 加 entry_signal_id/exit_signal_id 字段
- 新增 _resolve_signal_id() 在触发行回查 panel 列, 找出为 True 的具体信号
- 三个 simulate 方法加 entry_signal_ids/exit_signal_ids 参数
- strategy.py 把信号列表传给 engine, _trade_to_dict 透传新字段
- 前端 useSignalNames() hook 合并内置 (BUILTIN_SIGNAL_DEFINITIONS) + 自定义
  信号名称; TradeLegCell/ExitReasonBadge 显示具体信号名
- 信号名称前端解析 (复用 cnSignal), 后端只返回 signal_id, 不读磁盘

效果: 买入腿显示「MA5上穿MA20」, 卖出腿显示「MACD死叉」, 原因列从
通用「信号」变成具体信号名。止损/止盈/到期等非信号退出不受影响。

验证: 191 passed, 前端类型检查无错误
2026-07-12 18:31:36 +08:00
shy3130 ff059a0cda fix(backtest): 分钟K回测爆内存 — 按触发日精确读取 + 紧凑数组缓存
上一提交 (365f1cc) 修 index bug 时把 cache 从紧凑 numpy 数组改成臃肿的
polars DataFrame, 分钟K回测内存翻几倍导致 OOM 死机。且 _load_minute_for_fills
用 get_minute_range 扫描整个触发区间 (start~end), 触发日稀疏时读了大量无关
日期, 全市场长周期回测直接爆内存。

修复:
1. repository 新增 get_minute_by_dates — 按日期列表精确读 date=YYYY-MM-DD
   分区文件, 不扫描区间。内存与回测区间长度解耦, 只随触发日数量增长。
2. _load_minute_for_fills 改用 get_minute_by_dates, 每批 50 天分批读取。
3. cache 改回 float64 紧凑 2D 数组 (6 列: open/high/low/close/volume/amount),
   .cast(Float64) 保证 to_numpy 不退化回 object, 原来的 index bug 不复发。
4. _resolve_minute_fill 改用整数列索引 (arr[:,0]) 替代列名索引。

内存对比 (全市场 1 年):
  之前: get_minute_range 扫全年 ~365 文件 + DataFrame 缓存 → 5-10GB
  现在: 只读触发日文件 + float64 数组 → 几十 MB

验证: 191 passed (含 7 个分钟K回归测试)
2026-07-12 18:01:19 +08:00
shy3130 365f1ccbfb feat(minute-k): 分钟K同步流式落盘 + 段大小可配 + 回测成交修复 + 卡死超时调整
1. 分钟K流式落盘 (修复1年全市场卡死)
   - sync_minute_batch 加 on_segment 回调,每段拉完立即写盘
   - 抽出 _write_minute_partition 公共函数,消除重复
   - 内存峰值从全量(~25GB)降到单段,避免 OOM

2. 段大小可配 (默认20交易日,范围[5,30])
   - 新增 minute_sync_segment_days 偏好项
   - 「单次获取」与「获取1年」共用此分段设置
   - 前端设置弹窗新增分段大小步进器

3. 回测分钟K精确成交修复 (从未成功跑通过)
   - _resolve_minute_fill 改接受 DataFrame,避免 to_numpy() 退化为
     object 数组后字段名索引抛 IndexError
   - _load_minute_for_fills 用 partition_by 向量化分组替换逐行循环
   - 新增 test_minute_fill.py 回归测试 (6 用例)

4. 卡死 watchdog 超时阈值按任务类型区分
   - 普通任务 600s → 1200s;分钟K长任务 → 1800s
   - create(timeout_s=) 支持按 job 存阈值,reap_stale 读 job 自身值
   - 修复分钟K正常拉取被误杀导致僵尸线程继续写盘的问题

5. UI 优化
   - 设置弹窗按 自动同步/手动获取/清空 三区块分组
   - 「单次获取」改为按分段大小拉一段 (天数=分段设置)
   - 状态文案: 盘后自动同步 → 自动同步已开启/已关闭
2026-07-12 14:23:22 +08:00
shy3130 20ef070099 feat(backtest): 分钟K精确回测 — 穿越价/VWAP 成交 + Pro+ 门控
- engine.py: MatcherConfig 加 minute_fill; _resolve_minute_fill (穿越价/VWAP/降级)
  + _load_minute_for_fills; simulate_portfolio/independent_candidates 接入
- strategy.py: StrategyBacktestConfig 加 minute_fill
- backtest.py: strategy_stream 加 minute_fill 参数 + Pro+ 门控 + 数据范围检查
- repository.py: 新增 get_minute_range (多symbol x 日期范围)
- backtestTask.ts/StrategyBacktest.tsx: 激活 highGranularity 开关 + Pro+ 门控
2026-07-12 11:04:43 +08:00
im47cn db535c8004 feat(panelcache): IO 遥测量化跨折重复扫盘占比
walk-forward 各折 train 窗口日历重叠 ~75%, 但 PanelCache key 精确到区间,
跨折永不命中 -> 每折重复扫盘。为判断是否值得做"整区间加载一次+切片"优化,
先埋轻量遥测再决策 (避免盲优化)。

- PanelCache 累加真实扫盘耗时/次数 + 命中/single-flight 复用次数, 暴露 stats();
  BacktestEngine.cache_stats() 透传 (不让上层伸手进私有 _cache)。
- WalkForwardService.run 首尾快照, 返回 cache_telemetry.load_panel_pct
  (扫盘耗时/WF总耗时) + scans/hits/reuses, 并 logger.info 一行。
- 测试: PanelCache 计数 (miss扫盘/命中/并发复用); WF 测试补最小 engine 桩
  (生产路径不加 getattr 兜底, 避免静默吞掉真缺 engine 的 bug)。
2026-07-12 00:52:32 +08:00
im47cn 32d27a0a6e fix(walkforward): 子代理审查修复 — 折有效性/前视泄漏/方向退化/NaN序列化/净值曲线
三份子代理审查 (核心/API/前端) 发现的真实 bug:

核心正确性 (walkforward.py):
- [高] 折有效性抽象: best_params=None (训练全组失败) 或 OOS error 的折原会用默认
  参数硬跑/按0收益混入复利, 伪装成有效折污染 OOS 净值与退化指标。改为分流 ——
  有效折进 folds, 无效折进 skipped(带原因), 聚合只看有效折。一次修掉 H1/H2/M1。
- [高] 前视泄漏: train_end==test_start 使该日 K 线同时进训练与 OOS 首日。test_start
  后移一天隔断。
- [中] degradation 方向感知: 原 avg_is-avg_oos 对 min 类目标 (avg_holding_days) 符号
  反了。归一到越大越好空间再相减; 每折加 oos_degraded 方向感知标志。
- consistency 改为 OOS 盈利折占比 (与目标方向无关, 更直观)。

API (backtest.py):
- [高] 单折 guard: WF 缺 guard, start=None 默认拉 3 年, 每折训练窗口可能 OOM。按
  单折窗口 (train/test) 而非总区间 guard —— 总区间长本是 WF 正常形态, 按总区间拦会误杀。
- [中] NaN/inf 序列化: json.dumps(default=str) 处理不了 nan/inf, 输出非法 JSON 崩前端。
  加 _json_safe 递归清洗, 优化器与 WF 两处 done 分支都套上。

前端 (StrategyWalkForward.tsx):
- n_folds=0 门控: 全跳过时不再渲染误导性全0卡, 改显示'未产生有效折'+跳过原因。
- 渲染 OOS 拼接净值曲线 (walk-forward 核心产出, 原后端算了前端没画)。
- 每折退化标红改用后端方向感知的 oos_degraded (min 类目标 oos<is 未必退化)。

测试新增: 前视泄漏隔断 / 方向感知退化(min目标) / best_params=None跳过 /
OOS error跳过。后端 173 测试通过; 前端 tsc 无新增错误。
2026-07-11 19:20:31 +08:00
im47cn 586d9a4699 feat(walkforward): 真·walk-forward 优化后端 — 滚动窗口 IS 优化 + OOS 验证
PR2b 后端 (叠在 PR2a 优化器上):

app/backtest/walkforward.py:
- generate_folds: 滚动窗口切分 (训练固定长度 + 紧邻测试窗口 + step 前移),
  测试超出 end 即停; 放不下一折抛错; 非正窗口拒绝。
- aggregate_oos: 从各折 OOS 结果聚合 — 复利净值曲线 / IS-vs-OOS 退化
  (样本内目标均值 - 样本外目标均值, 正值=过拟合信号) / 一致性 (OOS 目标为正的折占比)。
- WalkForwardService.run: 每折在训练区间调 PR2a 优化器选最优参数, 再在测试区间
  用该参数做 OOS 回测。核心产出是纯 OOS 拼接 + 每折 IS/OOS 对比 — 单次样本内
  回测的过拟合一眼看穿。支持进度回调 (fold i/N) 与 cancel。

API app/api/backtest.py:
- GET /walkforward/stream (SSE, 复用 _BacktestJob + job_key 回吐) + POST
  /walkforward/cancel (按回吐 key 查表)。

测试 13 例: fold 切分 (滚动/边界/不足/非正) + OOS 聚合 (复利/退化/一致性/空) +
编排 (训练优化→测试OOS/退化上报/取消) + API (job_key 区分窗口/cancel按key)。
全量 169 测试通过。
2026-07-11 19:20:30 +08:00
im47cn 7b27c81dd0 perf(panelcache): single-flight 消除并发同 key 缓存踩踏
PanelCache 把 compute_fn 放锁外避免不同 key 串行化, 但代价是同 key
冷启动踩踏: 优化器/批量回测的 max_workers 个线程几乎同时 miss 空缓存,
全部在锁外并行调 compute_fn, 同一面板被并行加载 N 份 (N=并发线程数),
而非承诺的"只加载一次"。全市场 parquet 冷扫 ×N 的 IO/内存峰值。

改为 single-flight: 同 key 只推举一个 leader 执行 compute, 其余线程
登记为跟随者等 leader 结果复用。要点:
- compute 仍在锁外 → 不同 key 依旧并发, 保留原设计优点, 只消除同 key 重复。
- 失败不缓存: leader 抛错时摘除 inflight 让后续重试, 异常透传所有跟随者。

新增 2 个并发回归测试: 8 线程同 key 踩踏断言 compute 仅 1 次;
失败路径断言异常透传全部等待者且不缓存失败可重试。
2026-07-11 07:55:08 +08:00
im47cn ec3309163b feat(optimizer): 参数网格搜索优化器 (并行回测 + 目标排序 + SSE + 前端面板) (#82)
* feat(optimizer): 参数网格搜索优化器核心 + PanelCache 线程安全

PR2a 第一部分 (后端核心, 无 API/前端):

app/backtest/optimizer.py:
- expand_param_grid: 校验(类型/范围/选项) + 笛卡尔积展开; 支持显式候选值列表
  与 {min,max,step} 范围两种写法; GRID_MAX_COMBINATIONS=2000 硬上限防爆炸。
- StrategyOptimizer.optimize: ThreadPoolExecutor 并行跑各参数组回测, 按目标指标
  排序返回最优 + 全排名。objective 统一转越大越好(min 类目标取负), None/inf/
  失败组沉底。支持进度回调 (done/total/best) 与 cancel_event。
- 可选目标含 PR1 新增的 sortino/mc_maxdd_*。

engine.py PanelCache 线程安全:
- get_or_compute 整体加锁 — 并行优化器对同一 symbols/日期跑几十组参数时, 首个
  线程 compute 面板、其余等待后命中缓存, 同一面板只 scan_parquet+compute_all 一次
  (关键性能: 参数不影响面板 key)。max_size 2->4, ttl 180->900s 适配长任务。

测试 24 例: 网格展开/校验(未知参数/越界/选项/空/组合爆炸) + 编排(排序/每组一次/
min方向/失败None沉底/取消/进度/非法目标)。用假 service 注入受控 stats 验证。

* fix(optimizer): 子代理审查修复 — 异常隔离/展示符号/浮点端点/kwargs 校验

两份子代理审查发现 4 个真实 bug:

[高] 单组异常拖垮整批: _run_one 未捕获 service.run 异常, fut.result() 会
re-raise 冲出 as_completed → 整个网格搜索崩溃、已完成结果全丢。加了并行后
这个概率显著上升。用 try/except 隔离, 该组记为 error 继续。

[高] min 方向 best_score 符号错误: 内部把 min 目标取负做排序键, 但直接把
取负值当 best_score 返回 → 用户看到 avg_holding_days=-3.0 (负天数)。根因是
排序键与展示值混用。改为分离: 内部 _sort (取负空间, 不外露) + objective_raw
(原始展示值)。best_score/进度回调均用原始值。

[中] 浮点累加丢端点: v += step 累积误差使 0.1 步长丢失 max 端点
(0.1+0.1+0.1=0.30000004 > 0.3+1e-9)。改整数计数 lo + i*step。

[中] backtest_kwargs 非法/冲突 key: 展开传给 StrategyBacktestConfig 时若含
非法或保留字段 (symbols 等) 会在 worker 抛 TypeError, 被上述 #1 放大成崩溃。
入口加白名单校验, 提前明确报错。

测试新增 8 例: 符号还原/max_drawdown 负值排序/异常隔离/kwargs 非法+保留/
base_params 合并/浮点端点/去重。原 test_min_direction 只验 params 放过了符号
bug, 现补 best_score 断言。

* feat(optimizer): 参数优化 API SSE 端点 + 前端优化器面板

PR2a 完成 (API + 前端), 接上后端核心:

后端 api/backtest.py:
- GET /optimize/stream: 复用 _BacktestJob SSE 框架, 后台线程跑 StrategyOptimizer,
  progress_cb 推 done/total/best_score, 完成推 best_params/results 排名。param_grid
  走 JSON 字符串查询参数 (EventSource 仅支持 GET, 同 params/overrides 惯例)。
- POST /optimize/cancel: 从 query string 复原同一 job_key, cancel_event 停止。
- backtest_kwargs 透传各回测参数 (matching/fees/mode/...) 到每组回测。

前端:
- lib/optimizerTask.ts: SSE 客户端 (镜像 backtestTask), 进度/结果/重连/取消。
- pages/backtest/StrategyOptimizer.tsx: 配置面板 (选策略 → 勾选可扫参数设
  min/max/step, bool/select 自动全扫; 优化目标下拉; 日期; 组合数实时预估 + 2000
  上限提示) + 结果面板 (最优参数高亮 + 排名表: objective/夏普/索提诺/收益/回撤/
  胜率/交易数)。
- Backtest.tsx: 新增 '参数优化' 第三 tab。

测试 test_optimizer_api.py 3 例: job_key 确定性 + 区分 grid/objective + stream 与
cancel 复原同一 key (守护 PR3 C1 类失配)。前端 tsc 无新增类型错误。

注: SSE 端到端需真实日K数据, 本地 mode=none 无法验证实际回测; 但 SSE 管线镜像
已测的 strategy_stream, 优化器核心 24 单测覆盖。

* fix(optimizer): 子代理审查修复 — API 取消/空网格/方向对齐 + 前端重连/切换/展示

两份子代理审查(API + 前端)发现的真实问题:

后端 API:
- [中] param_grid 为 null/[]/'' 等合法 JSON 但非网格对象时, 原逻辑跳过线程却不置
  job.done -> event_generator 永久空转、job 挂死在表中。改为非空 dict 校验拦下。
- [中] 取消后前端收到 done 而非取消提示: 优化器把 cancel 当每组失败正常返回 dict,
  done 分支照推'完成'。改为 done 分支先检查 cancel_event, 分流为'优化已取消'。
- [低] direction 空串边界: stream 侧 '' 与 cancel 侧 or None 口径不一致致 job_key
  失配(cancel 失效)。stream 加 direction = direction or None 对齐。

前端:
- [中] tryReconnectOptimize 是死代码(无调用方)违反 NO DEAD CODE: 接入 useEffect
  挂载恢复(镜像 StrategyBacktest 的 tryReconnect), 刷新/切页后恢复未完成优化。
- [中] 切策略后旧结果残留错配(参数列是旧策略): onSelectStrategy 加 clearOptimize。
- [低] 排名表 slice(50) 静默截断: 加'仅显示前50/共N组'提示。
- [低] objective_raw 裸数与 best_score 精度不一: 统一 toFixed(3)。

后端 76 测试通过; 前端 tsc 无新增类型错误。

* refactor(optimizer): job_key 回吐 — 消除 cancel 两侧重算的脆弱契约

采纳子代理审查建议, 从结构上根除整类 job_key 失配 bug:

之前 cancel 需从 query string 逐字段重算 job_key, 必须与 stream 侧完全一致 —
任何默认值/None-空串/类型转换漂移都静默导致取消失效 (PR3 C1、本轮 direction
空串失配都是这个结构的产物)。

改为: stream 首个 SSE 事件 (event: job) 回吐后端算出的 job_key, 前端存下,
cancel 直接原样传回按 key 查表。cancel 侧不再重算, 契约漂移无从发生。

- 后端 optimize_stream: 首事件 yield event: job {key}; optimize_cancel 简化为
  body.job_key 直接查 _running_jobs (删除 40 行 qs 重算)。
- 前端 optimizerTask: 监听 job 事件存 currentJobKey + localStorage; stopOptimize
  改传 {job_key}; done/error/cancel 清理 key。
- 测试: 原 stream/cancel qs 对齐测试已无意义, 改为验证 cancel 按回吐 key 查表
  (命中/已完成/未知 key 三态), 用轻量 fake Request 直调 endpoint。

后端 76 测试通过; 前端 tsc 无新增错误。策略回测路径未动 (已合并 + C1 测试守护),
本重构仅限本 PR 新增的优化器路径。

* chore(optimizer): 移除冗余 PanelCache 改动 — main 已独立实现线程安全

rebase 到 main 时发现上游已独立给 PanelCache 加锁 (且 compute 放锁外, 比本 PR
原方案更优), 并新增 asset_type 维度。故本 PR 的 PanelCache 改动 (加锁 + size/ttl
bump + docstring) 全部冗余且 docstring 已与 main 实际锁行为不符, 回退到 main 版本。
优化器共享单一 panel key, main 的 PanelCache 已完全够用。

至此本 PR 零 engine.py 改动。

* fix(optimizer): 处理 #82 review 的 6 处问题

作者 review #82 提出的阻塞/改进项, 逐条修复:

[阻塞1] 前端构建失败: EmptyState 只接受 title/hint, 误用了 description ->
npm run build (tsc -b) 报 TS2322。改为 hint, build 通过。

[阻塞2] 优化没用用户当前策略配置: optimize API/前端只传 strategy_id/param_grid/
objective/日期/mode, 未传 params/overrides。补齐 —— API 新增 params(base_params)/
overrides 两个 query 参数并纳入 job_key; 前端把选中策略的 params_defaults 作为未扫描
参数固定值, buildDefaultOverrides(strategy) 让 basic_filter/信号/风控按当前策略参与。
抽 lib/strategyOverrides.ts 共享 (与策略回测页同口径, 避免重复)。

[阻塞3] 切策略丢失运行中任务控制权: 原 clearOptimize 只清前端状态, 不关 SSE/不 cancel
后端/不清 localStorage -> 后端继续跑但 Stop 消失。改为: 有任务在跑时切策略先 stopOptimize
(真正 cancel + 关连接 + 清存储)。

[阻塞4] 停止按钮竞态: job_key 只在收到首个 job 事件后才有, 刚点开始就点停止时前端还没
key, cancel 落空。改为: stopOptimize 标记 cancelRequested, 有 key 立即 POST cancel,
无 key 则保持 SSE 等 job 事件到达时补发 cancel 再关 (关 SSE 不停后端 daemon 线程, 必须
真 POST); 加 5s 兜底。

[改进5] SSE 断线健壮性: 无 data 断线原全靠浏览器自动重连无上限。加 MAX_RECONNECT=5,
超限置 error 停 pending。

[改进6] 组合数校验与后端不一致: 前端 round((hi-lo)/step)+1 会把 min=0/max=1/step=0.6
显示为可运行 3 组, 但后端生成末值 1.2>max 报错。新增 sweepError 与后端 _candidates_for
同口径校验 (步长不整除/越界), 前端提前拦并禁用运行。

后端 156 测试通过; 前端 npm run build 通过。
2026-07-10 11:38:30 +08:00
wshy 6bb6eadb12 fix(storage): 兼容 parquet 分区 schema 演进
兼容 quote_ts 等新增存储列导致的新旧 parquet 分区 schema 差异,恢复老用户 enriched 缓存刷新和看板/选股页面显示。
2026-07-09 17:38:44 +08:00
im47cn 61d1f28fd8 polish(backtest): 采纳 #67 审查建议 — MC clip 防御 + Sortino 口径注释 + 标签澄清 (#81)
作者 review PR #67 时提的 3 条非阻断建议, 本 PR 采纳:

1. 蒙特卡罗回撤加 np.clip(pnls, -0.9999, None): 单笔 pnl <= -100% 时 (1+pnl)<=0
   会让 cumprod 符号翻转、净值非正、回撤失真。回测有止损实际不发生, 纯防御兜底。
   加测试 test_mc_drawdown_clips_sub_minus_100pct_pnl 用 -150% 输入验证分位仍有界。
2. _calc_stats 逐笔 Sortino 沿用 sharpe 的 sqrt(252) 基准, docstring 注释点明这是
   '内部一致 > 局部绝对' 的刻意选择, 免未来审查者重复质疑。
3. 前端标签 '蒙卡回撤(95%最坏)' → '(95%置信不差于此)', 避免误解为 95 分位值。

全量 130 测试通过 (含 upstream 已修的 trailing 测试, issue #66 已解决)。
2026-07-09 16:48:13 +08:00
Jinfeng SunandClaude Opus 4.8 9aa96edbd7 改进: 并发韧性 + 数据性能 + 死代码清理 + 前端 UX + ST/Sharpe 修复 (#78)
* fix(concurrency): 共享缓存/任务表加锁, 全局限速, depth 原子写, 认证热路径缓存

修复多线程下的竞态与阻塞:
- overview/strategy_cache/PanelCache/StrategyMonitor._watching 四处共享状态加锁,
  消除 "dict/OrderedDict mutated" 与丢更新/半写读取
- strategy_cache/depth parquet 改临时文件 + os.replace 原子写
- rate_limits 改进程级共享时间轴限速, 并发同步不再聚合超过单能力 rpm;
  scheduler 令牌账目与 sleep 分离, sleep 不再独占锁串行化其他请求
- auth.is_configured() 内存缓存, 认证中间件不再每请求读盘阻塞事件循环
- api/backtest 任务清理/取消全程持 _jobs_lock, 并用 Semaphore(2) 限并发重回测

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(data): limit_ladder 去 N+1 全市场重算, 指标裁剪, factor 向量化

- limit_ladder 前一日 consecutive 改窄读单日 parquet 存储列 (谓词/投影下推),
  替代 range(1,10) 逐日 _load_enriched_for_date 全市场指标重算 (最坏 9x)
- compute_indicators 新增可选 needed 裁剪 (默认 None 行为逐位不变, 已对照验证),
  factor 只算所需因子列
- factor._calc_period_return 用 Polars join 替代 Python 逐行 price_map 循环,
  _add_groups 去 map_elements 改纯表达式 (输出逐位一致)
- screener ext value_map 按 parquet mtime 记忆化, 免每请求磁盘重读
  (DuckDB 过滤仍用隔离 :memory: 连接, 不扩大注入面)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(backend): 报表存储去重, 删死代码, DuckDB 视图重建收敛, 管道失败如实标记

- 三份近乎逐字复制的 *_reports.py 收敛到共享 JsonReportStore (原子写 + 锁),
  各模块公有 API/id 格式/上限/落盘 schema 完全保持不变
- 删除 ext_pull.py 中字节相同的死 _run_loop (Python 只绑第二个) 及无用 import
- 13 张 DuckDB 视图重建收敛为唯一权威 repository.rebuild_views(),
  daily_pipeline 与 /api/data/clear 改为调用 (修好 clear 路径漏挂视图的漂移)
- daily_pipeline 累积 stage_errors 并在末尾抛出, 部分失败不再误报成功;
  free/None 模式的能力门控跳过不计入失败

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(frontend): SSE 连接态, 路由代码分割, 查询失效修复, 三态与无障碍

- 实时行情 SSE: 连接态 store + 指数退避 + 断线徽标/toast (避免静默丢告警);
  回测 SSE 断线有界重连 + 可重试, 不再永久卡住进度条
- router 全部 React.lazy + Suspense, vite manualChunks 拆图表库
  (echarts 变独立 1MB 懒加载 chunk, 首屏包显著减小)
- 修 Data 清库后其它页显示旧数据 (改回广域失效); 修 Watchlist kline 失效键
  永不匹配; query key 收敛到 QK 工厂 (新增 strategyDetail)
- Monitor/Analysis/StockAnalysis/ExtPages/CustomSignals 补 loading 门控与
  error/empty 三态区分
- 新增共享 Modal 原语 (焦点陷阱/ESC/焦点还原/aria), 改造 3 个高频弹窗;
  Toast/AlertToast 加 aria-live 与键盘可达; Watchlist/LimitUpLadder 卡片 memo

修复本轮 review 发现的缺陷:
- Modal 焦点 effect 依赖 onClose 致每次输入抢焦点 → 改 ref 只装一次
- StrategySettingsDialog 删除确认框被 Modal 面板裁剪 → 移出作兄弟节点

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(quant): 修正 ST 板块限价套错 与 因子 Sharpe 年化频率

两个不报错但会算错数的领域 bug:

1. ST 5% 涨跌停限幅被无条件套到创业板/科创板 ST 股:
   注册制改革后 创业板(300/301)、科创板(688/689) 的风险警示股仍执行 20%,
   北交所 30%, 只有主板 ST 才是 5%。原代码 _is_st 先判且覆盖板块限幅, 导致
   创业板/科创板 ST 的涨停价按 5% 计算 → +5% 被误报涨停、真 +20% 涨停被漏报,
   污染 signal_limit_up / consecutive_limit_ups / 连板梯队 / near_limit_up。
   修正: ST 5% 仅在 ~(创业板|科创板|北交所) 时生效 (EOD + 盘中两条路径 + near_limit_up)。

2. 因子回测 Sharpe 一律乘 √252, 但 group_nav 每点是一个调仓周期收益:
   月频调仓下是月收益, 乘 √252 会把 Sharpe 高估 √(252/12) ≈ 4.6x (周频 ≈2.2x),
   使无效因子显示成明星因子, 废掉"先筛无效指标"的用途。
   修正: 年化系数按 config.rebalance 取 √252/√52/√12。

新增 tests/test_st_limit_and_sharpe.py (5 例) 覆盖两处修正。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 18:10:19 +08:00
Jinfeng SunandClaude Opus 4.8 e5a94c42d5 feat: ETF 支持(选股 / 回测 / 监控) (#61)
* feat(screener): 选股引擎支持 ETF

- 12 个内置策略打 asset_types 白名单 + strategy_supports_asset;涨停类
  (连板/断板反包)仅股票,其余 10 个技术类对 ETF 开放
- ScreenerService(repo, asset_type) 分流取数,ETF 复用 kline_etf_enriched,
  跳过股票专用历史缓存与涨停信号;进程级 _history_cache key 含 asset_type
- API /run、/run_preset 透传 asset_type;/strategies 按资产过滤;
  股票专有策略在 ETF 下返回空
- 新增 enriched_dirname(asset_type) 共享 helper;get_enriched_latest_asset
  增 refresh 参数(供轮询线程避免冷缓存同步重算)
- 前端「策略」页加 股票/ETF 切换,ETF 走实时单跑(空日期→用 ETF 自身最新日);
  QK.screenerStrategies 按 asset_type keyed
- 测试:test_screener_etf.py

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(backtest): 回测支持 ETF(个股/因子/策略组合)

- 三条回测路径 + 共用 BacktestEngine 面板加载按 asset_type 路由到
  kline_etf_enriched(复用 enriched_dirname);PanelCache key 隔离资产;
  ETF 跳过股票专用 get_enriched_range 缓存
- 面板 compute_all/名称 JOIN 按 asset_type 取维表(get_instruments_asset),
  修复 ETF 策略回测用错股票维表致名称为空/涨停信号算错
- BacktestConfig/FactorConfig/StrategyBacktestConfig 增 asset_type
- 三个回测 API + SSE stream 透传 asset_type;_make_job_key 纳入 asset_type
  (修复 stream 与 cancel job_key 不对齐致取消失效的回归)
- 前端策略组合页/因子页加 股票/ETF 切换,标的搜索与策略列表跟随资产;
  assetType 持久化
- 测试:test_backtest_etf.py(含 job_key 一致性回归);既有回测测试替身同步

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(monitor): 监控规则支持 ETF

- engine.evaluate(df, asset_type) 按规则 asset_type 分轮评估;quote_service
  增开 ETF 评估轮(用 ETF enriched 快照),股票轮不受影响、不重置其策略结果
- ETF 评估轮独立 try(异常不丢弃已算出的股票告警)+ refresh=False(不在轮询
  线程触发 ETF 冷缓存同步重算)
- ETF 版历史加载器(main.py 注入)+ 按规则 asset_type 选加载器
- _strategy_pools 按 (sid, asset_type) 键,避免同策略股票/ETF 规则互相覆盖
- name_map 仅在有 ETF 规则时补 ETF 维表, setdefault 保股票名优先
- RuleModel/normalize 增 asset_type(默认 stock,持久化往返)
- 前端 RuleEditor 加 股票/ETF 选择,策略列表与标的搜索跟随资产
- 测试:test_monitor_etf.py

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(etf): 前端 API 绑定透传 asset_type + 文档

- api.ts: screener/backtest 绑定加 assetType 参数,MonitorRule 类型加 asset_type
- docs/features.md: 标注选股/回测/监控的 ETF 支持范围与前提

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(reliability): 管道并发/原子写/能力探测/监控告警多处加固

后端可靠性专项修复(均带回归测试, backend 全套 64 passed):

并发与数据完整性:
- 盘后管道单飞: JobStore.create() 去重纳入 pending∨running, 关闭"两次快速点击"
  并发双跑窗口; 新增 _heavy_run_lock 执行槽挡住 reap 后僵尸线程并发写 parquet
- adj_factor/minute 全部改走原子写(tmp+replace), 消除 kill/断电致 all.parquet 损坏
- 分块拉取失败聚合 WARNING 可见化(不再静默当成功); 复权失败标的会保持旧价已提示

能力探测:
- 周期重探(60min)热更新 app.state.capabilities, 付费 Key 过期/续费无需重启即可见
- 瞬时探测失败(超时/连接/5xx, 按 _is_transient 判定)不降级、保留旧付费档;
  真 401/无权限仍正常降级回落 free-api

监控告警:
- 评估仅在连续竞价(9:30-11:30/13:00-15:00)+ 快照当日新鲜度下进行, 避开集合竞价/
  收盘后陈旧价与节假日误告警
- scope=sector fail-closed(validate 拒绝新建 + _apply_scope 返回空), 修复板块规则
  对全市场刷屏
- 飞书 webhook 加退避重试并移到独立线程池 fire-and-forget, 不再阻塞行情轮询线程

单标的新鲜度: 新增 repo.symbols_lagging() 检测掉队标的并 WARNING + 计入 job 结果

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 12:12:29 +08:00
Lytem 9c730e4b69 fix: 修复因子回测分层错误问题 (#63)
* fix: force LF for uv.lock

* Fix factor backtest grouping stability
2026-07-08 12:11:03 +08:00
im47cn 3a5f3e05b6 feat(backtest): 稳健性指标 Sortino + 蒙特卡罗回撤分位 + per-trade 明细 (#67)
* feat(backtest): 新增稳健性指标 Sortino + 蒙特卡罗回撤分位 + per-trade 明细

回测原本只有 Sharpe/Calmar/最大回撤, 缺防过拟合视角。本 PR 补三类:

1. Sortino 比率: 用下行偏差 (MAR=0 目标半方差) 替代总标准差, 只惩罚负
   收益波动。无亏损时约定返回 None (不虚报 0/inf)。各 stats 函数用与自身
   Sharpe 相同的收益基准 (逐笔 or 日频)。
2. 蒙特卡罗最大回撤分位: 对逐笔收益有放回重抽样 1000 次, 估计回撤分布,
   报 P50(中位) 与 P95(95% 置信最坏)。回答"仅因成交顺序运气回撤能有多坏",
   单次样本内回撤看不到这个。固定种子 (42) 保证可复现/可测。
3. per-trade 明细: best/worst/median_pnl/avg_holding_days, 补 _calc_stats
   与 _calc_portfolio_stats 原本缺失的逐笔视角。

三个 stats 函数 (_calc_stats / _calc_independent_candidate_result /
_calc_portfolio_stats) 全部接入, 用共享静态 helper (_sortino_ratio /
_mc_drawdown_percentiles / _per_trade_block) 避免重复。纯 additive, 既有
字段不变, 空交易安全。

前端 StrategyBacktest 结果区新增 索提诺 / 蒙卡回撤(中位) / 蒙卡回撤(95%最坏)
三个指标卡 (stats 为 Record<string,any>, 无需改类型)。

新增 tests/backtest/test_robustness_metrics.py 11 用例: Sortino 手算校验/
无下行 None/优于 Sharpe/样本不足; 蒙卡确定性/P95≤P50≤0/全正零回撤/样本不足;
_calc_stats 与 portfolio 字段集成 + 空交易安全。

注: upstream main 既有 test_trailing_take_profit_exits_after_activation
失败与本 PR 无关 (改动前后一致复现)。

* test+fix(backtest): 子代理审查修复 — 补 full 主路径覆盖 + finite 护栏 + 内存上限

子代理审查发现的缺口, 本次补全:

覆盖 (测试):
- [关键] full 模式主路径 _calc_independent_candidate_result 原无集成测试,
  漏拼 sortino/mc 字典展开会导致前端指标卡空值却测试全绿。新增该分支断言。
- MC 确定性测试从"两次相等"升级为钉死快照值 (p50=-0.0976/p95=-0.2108),
  一旦有人把种子改成系统熵立即红。
- p95 断言从恒真的 p95<=p50 (percentile 单调性数学恒成立, 无信息) 改为
  p50<0 且 p95<p95 严格更差, 真正验证数值。
- 补全盈利交易 → sortino 集成层为 None 的用例 (原仅 helper 层覆盖)。

健壮性 (实现):
- 三个 helper 入口 finite 过滤: inf/nan 收益会让 cumprod 传播 nan → 分位 nan
  → asdict 直出非法 JSON (项目 _safe 只洗 rows 不洗 stats)。剔除后与纯净输入
  结果一致 (加测试守护)。
- MC 内存护栏: full 模式可数千笔, n_sims×n×8B×4 数组瞬时可达数百 MB。
  按 2M 单元上限压降模拟次数 (小样本 n_sims=1000 不变, 快照稳定)。

注: 逐笔年化口径 (_calc_stats 对 pnls ×√252) 落在死路径 simulate() (无 app
调用方) 且复刻既有 Sharpe, 不在本 PR 范围。两条活路径 (portfolio/independent)
均用日频基准, ×√252 正确。
2026-07-08 12:10:05 +08:00
shy3130 f8914c9140 feat: 新增企业微信推送、个股分析页重构、价位算法优化及多处交互改进 (v0.1.82)
- 推送: 新增企业微信群机器人通道(与飞书并列), 监控告警/复盘报告多渠道分发
- 个股分析: 历史报告改为右侧常驻栏(展示全部股票报告), 进页面自动恢复上次选股
- 价位算法: 成交密集区改用换手率衰减模型(国内主流筹码分布); 缺口回补判定修正(必须完全穿越缺口)
- 回测: 回撤止盈改纯峰值口径(与 trailing_stop 一致); 佣金/印花税/滑点合并一行; 建仓口径加问号气泡说明
- 分时图: 自选列表迷你分时图加渐变填充(对齐个股对话框风格)
- 图表: 个股分析日K图右侧标签适配浅色主题; 价位标签与下方文字行双向 hover 高亮联动
2026-07-07 17:56:25 +08:00