Commit Graph
161 Commits
Author SHA1 Message Date
im47cn b4fa9d6eb4 fix(optimizer): SSE 放弃时清 localStorage + JSON 解析失败告警
对抗式审查发现的两处健壮性问题 (与 walk-forward 同源, 此为优化器侧):
- stopOptimize 延迟分支 + MAX_RECONNECT 超限退出漏清 localStorage, 导致刷新页面
  tryReconnect 会重连到已放弃/未取消的任务。两处补 removeItem(RECONNECT_KEY/JOB_KEY_KEY)。
- optimize_stream 的 params/overrides JSON 解析失败静默降级为 {}/None, 会让用户配置
  丢失变成无声 bug。升级为 logger.warning (仍降级不崩流, 但可诊断)。
2026-07-12 22:22:36 +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 2014da57a9 Merge PR #103: Codex CLI 增加 GPT-5.6 支持 + 安全隔离
冲突解决: 保留自定义预设 + GPT-5.6 模型选择 + reasoning_effort 配置
2026-07-12 11:07:16 +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
shy3130 fc85d53bee feat(minute-k): 分钟K重做 — 前复权 + 向前扩展 + 清空 + 单独拉取 + 进度
- kline_sync: adjust="forward" 前复权; sync_minute_batch 按月分段(57天/段)
  突破 count=10000 上限; on_chunk_done 回调带日期段标签
- kline.py: 删除 extend_minute_history; 新增 sync_minute_single(单股拉取落库)
  + clear_minute(仅清分钟K二次确认) + sync_minute 可选 days/extend 参数
- preferences/settings: 默认 5 天上限 30 (恢复原始); 超时强制释放 _heavy_run_lock
- MinuteSyncConfig: 自动同步(5天/关闭) + 往前获取(单次拉满) + 获取1年(分段)
  + 清空按钮 + 进度回调 onJobStart
- StockIntradayChart: 单股获取改用 syncMinuteSingle (落库)
- daily_pipeline: _minute_chunk_progress 兼容 seg_label 参数
2026-07-12 11:04:34 +08:00
Lzwaang 646d143d58 feat: 完善 Codex CLI 模型配置与安全隔离 2026-07-12 00:31:03 +08:00
shy3130 7e6ea3afcd feat(custom-signals): 自定义信号条件支持日期偏移 + 字段弹出选择器
- custom_signals.py: 新增 _col(name, days) helper (days>0 时 .shift().over('symbol'));
  validate 加 leftDays/rightDays 校验 (0-60); build_expressions 加 allow_shift 参数
- signals.py: ConditionModel 加 leftDays/rightDays; options 端点加 maxDays + 字段分组 groups
- pipeline.py: 盘中路径 build_expressions(allow_shift=False), 带偏移的信号优雅跳过
- api.ts: CustomSignalCondition 加 leftDays/rightDays; options 加 groups/maxDays 类型
- CustomSignalDialog.tsx: 字段选择改为 Portal 弹出式 (搜索+分组), 解决下拉框过长;
  日期偏移控件 (最新/前N日 两种态); 宽松布局
- CustomSignals.tsx: 只读卡片用 fieldWithDays 显示偏移标注

向后兼容: 旧 JSON 无 leftDays/rightDays 时当 0 处理, 行为不变
2026-07-11 19:15:54 +08:00
wshyandshy3130 fcae7f5446 feat(dashboard): 看板间距收紧、卡片美化、板块标识与连板个股名称 (#101)
- 收紧看板全局间距(卡片间 gap/外层 padding 降至 6px),信息密度提升
- 卡片统一加微投影/毛玻璃/hover 浮起效果,标题加渐变指示条
- 四榜与概念/行业热度龙头个股加创/科/北板块标识
- 涨停梯队(后端 tiers 带 stocks)及最高连板卡片:≤3 只时显示个股名称

Co-authored-by: shy3130 <shy3130@users.noreply.github.com>
2026-07-11 16:57:23 +08:00
shy3130 76fbd70e13 fix(ext-data): 行业/概念页拉取兼容字典信封, 修复新用户「接口返回不是数组」
_fetch_json 原硬性要求上游返回裸数组 [{...}], 部分用户网络环境
(CDN/WAF/网关) 会把数组包成 {data:[...]} 信封或返回错误 dict,
导致新用户首次「获取数据」必现 HTTP 400 弹窗。

- 兼容 {data/list/rows/result/results} 等常见信封键, 兜底遍历取首个数组
- 真不是数组时, 错误信息附带响应预览(截断200字)便于排查
- 通用拉取引擎已有 response_path 解包, 预设路径此前绕过了它
2026-07-11 13:17:56 +08:00
shy3130 56ee0e6c0f feat(signals): 新增价格上穿/下穿 MA5 MA10 信号
- pipeline.py: compute_signals() 与 compute_enriched_today() 双路径
  新增 signal_ma5_breakout/breakdown、signal_ma10_breakout/breakdown
  (盘中路径同步补 _prev_ma10 到 sig_prev)
- ENRICHED_COLUMNS 登记新列
- signals.ts: BUILTIN_SIGNAL_DEFINITIONS 加 4 条定义
- monitor.py: _SIGNAL_CN 加 4 条中文标签

注: 价格上穿/下穿 MA20 已有 (signal_ma20_breakout/breakdown), 本次补齐 MA5/MA10
2026-07-11 13:17:45 +08:00
shy3130 17994445e2 feat(voice): 监控告警语音播报 (播报个股名称与信号) v0.1.84
- 新增 voiceBroadcast.ts: 浏览器原生 speechSynthesis, 零依赖/零后端改动
- 按 source 分类拼接文案, 必念个股名称不念代码, 含涨跌幅
  · strategy: [名称] 进入/移出 策略「名」 [涨跌幅]
  · signal:   [名称] 买入/卖出信号触发 [涨跌幅]
  · price/market: [名称] [条件摘要] [涨跌幅]
- 默认偏好 Google 中国大陆语音 (音质最佳), 异步注入自动同步回显
- 整批合并一句 + 正在播报时丢弃新批次 (防快行情叠噪音)
- AlertToast 加 dispatchSideEffects 聚合点: 弹窗逻辑与副作用职责分离
- System 设置页加语音播报区: 开关(默认关)/音色/语速/试听
- 版本号同步升至 0.1.84 (VERSION / __version__ / package.json)
- README 监控中心加语音播报特性说明
2026-07-11 11:21:57 +08:00
wshy 49a259d94d Merge pull request #95 from im47cn/perf/panelcache-single-flight
perf(panelcache): single-flight 消除并发同 key 缓存踩踏
2026-07-11 10:54:39 +08:00
shy3130 bdbce71217 feat(ext-data): 概念/行业定时拉取默认开启 + 修复启动顺序
- 概念/行业预设 enabled 由 False 改为 True, 全新部署首次启动即自动调度
- 修复 main.py 启动顺序: ensure_builtin_presets 移到 pull_scheduler.refresh
  之前, 否则全新部署时 scheduler 读不到刚创建的预设, 定时任务不启动
- 已有用户遵循"已存在则跳过"原则, 不受影响
2026-07-11 10:44:37 +08:00
wshyandshy3130 773cd2fe12 chore(polling): 调整各档位轮询间隔范围 (#96)
* fix(ai): 兼容 reasoning 模型 temperature 限制 + 透出上游真实错误

Kimi kimi-k2.7-code 等 reasoning 模型拒绝非约定 temperature (Moonshot
报 "only 1 is allowed for this model"), 之前无条件下发导致 400 配置失败。

- 捕获 temperature 相关 400 后自动去掉 temperature 重试一次 (非流式 + 流式),
  不再依赖模型名猜测, 对任意 reasoning 模型稳健
- _format_openai_error 优先透出上游真实 detail, 仅无可读 detail 时回落到
  状态码通用文案, 避免吞掉 "model not found" 等排障关键信息
- 前端 Kimi 预设 model 更正为 kimi-k2.7-code

* chore(polling): 调整各档位轮询间隔范围

实时行情 (quote_service):
- pro 最小间隔 2s → 3s, starter 3s → 6s (expert/free 不变)
- DEFAULT_INTERVAL 10s → 6s

五档盘口 (depth_service):
- expert 区间上限 300s → 120s
- 默认值 20s → 10s

前端兜底默认值同步 (Data/Monitoring/DepthConfigCard):
- quote interval fallback 10→6, min 5→6
- depth interval fallback 20→10, expert hi 300→120

---------

Co-authored-by: shy3130 <shy3130@users.noreply.github.com>
2026-07-11 10:22:11 +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
shy3130 e61c228002 fix(ai): base_url 归一化保留非 v1 版本段, 修复 GLM /v4 被 404
normalize_openai_base_url 旧实现只检查「是否以 /v1 结尾」, 不是就强制补 /v1。
智谱 GLM 的 OpenAI 兼容地址是 /api/paas/v4, 被补成 /api/paas/v4/v1,
SDK 再拼 /chat/completions → 不存在路径 → 404。

改用正则检测 URL 末尾已有的版本段 (/v1、/v2、/v4、/v1.1 等):
- 已有版本段 → 保持原样
- 无版本段 → 补 /v1 (无版本号网关行为不变)
- 去 /chat/completions 后缀逻辑保留

补 4 条单测覆盖 GLM /v4 场景, 原有 v1/无版本网关回归不变。

Closes #60
2026-07-10 17:06:17 +08:00
shy3130 d9fe050b1f fix(realtime): 策略页实时行情闪烁 + 策略结果刷新机制
策略页开启实时行情时, 被监控的策略列表反复闪烁 (变 0 → 全部失效 → 又出现),
非监控策略不受影响。

根因: 每个行情周期后端先广播 quotes_updated 再重算策略, 重算时先清空内存结果
再逐个回填 (非原子窗口)。前端 quotes_updated 与 strategy_results_updated 两个
SSE 事件都刷新 screener-cached, 第一次撞上清空窗口拿到空结果, 第二次拿到重算
结果, 每周期重复 → 闪烁。

修复:
- 前端: 从 SSE_INVALIDATE_PREFIXES 移除 screener, quotes_updated 不再刷新策略页,
  每周期只剩 strategy_results_updated (重算完成后才发) 触发一次刷新。
- 后端: monitor.evaluate 改为临时容器收集结果, 算完后整体替换 _latest_strategy_results,
  /cached 并发读取永远拿到完整结果, 不会读到空中间态。

配套:
- 新增 strategy_results_updated SSE 事件 + subscriber 合并通知机制
- 策略卡片 loading 时不显示旧命中数, 避免刷新时数字跳动
2026-07-10 16:37:11 +08:00
shy3130 fdee661d47 fix(watchlist): 保留冷缓存自选占位行 2026-07-10 15:07:14 +08:00
shy3130 be626c73e1 release: v0.1.83 2026-07-10 12:00:49 +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 6076074ac0 fix(realtime): unify turnover rate units (#93) 2026-07-09 19:14:50 +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
wshy 1840e65f06 fix(indicators): 量比改为同花顺/东财标准算法 + 行情时间戳落盘 (#91)
原 vol_ratio_5d 有两个叠加缺陷导致盘中严重偏低:
1. 分母含自身(rolling_mean(5) 算了当天) → 量比偏低约1.4倍
2. 盘中无时间折算(部分量直接比全天量级) → 开盘时段极低

改为标准量比公式(同花顺/东方财富/通达信一致, 已查证官方定义):
  量比 = 今日累计成交量 / (前5日均量 × 已交易分钟数/240)
- 分母不含当天(volume.shift(1).rolling_mean(5) / tail(5)前5日)
- 盘中按已交易分钟数折算(×240/elapsed), 盘后系数=1.0不受影响

已交易分钟数用行情 quote_ts 推算(比服务端时间更准, 无网络延迟):
- market_time.py: trading_minutes_elapsed_from_ts(毫秒时间戳)
- normalizer.py: DAILY_COLS 加 quote_ts, 保留 SDK timestamp
- quote_service.py: _build_daily 保留 quote_ts, 传入 elapsed_minutes
- pipeline.py: 两路径(盘后全量/盘中增量)改为标准算法 + ENRICHED_STORAGE_COLS 加 quote_ts
- repository.py: live_agg 新增 _vol_ma5_prev_sum(tail(5)前5日和)

quote_ts 落盘后还可用于: 盘后收盘数据校验(非15:00重拉)、跨天完整性检查。

不改动: vol_ma5/vol_ma10保留原语义(含当天均量), 策略/选股阈值不变。
129后端测试全过, EOD量比验证正确(707636/100000=7.076)。
2026-07-09 16:41:02 +08:00
wshy 000fcba0ed fix(strategy): 实盘选股 lookback 窗口用自然日裁剪导致偏少 (#90)
策略 LOOKBACK_DAYS 语义是交易日(见 strategy-guide.md), 但实盘选股路径
用 timedelta(days=N) 按自然日裁剪历史窗口。N 个自然日 ≈ N×5/7 个交易日,
周末/节假日让窗口系统性偏少, 导致 filter_history 策略的滚动窗口/行号差
(_gap)漏算, 实盘与回测结果不一致。

- repository.py get_enriched_history: 从数据实际交易日序列取最后 N 个交易日
- screener.py _load_enriched_history: 同上(慢路径)
- 不动 warmup 裁剪(×2 放宽, 多读无害)和 run_all cap=30(性能保护, 独立问题)
- 回测路径本就正确(传完整 panel, 无裁剪), 不受影响

验证: lookback=10 旧逻辑返回 7 交易日 → 新逻辑返回 11 交易日(含target当天)
129 后端测试全过, 无回归
2026-07-09 16:35:43 +08:00
wshy e0badf1458 feat(wecom): 智能机器人长连接接入(配置项 + 保活) (#89)
* feat(wecom): 智能机器人长连接接入(配置项 + 保活)

新增企业微信智能机器人(API模式/长连接)通道, 与群推送 Webhook 并存。

后端:
- wecom_bot_service: WebSocket 长连接管理器, daemon 线程内跑 asyncio
  连接 wss://openws.work.weixin.qq.com → aibot_subscribe 鉴权
  → 30s ping 心跳 → 断开指数退避重连(min(base*2^(n-1),60s))
  结构对齐 depth_service(start/stop/boot_check/apply_credential_change)
- preferences: 新增 wecom_bot_id/secret/enabled getter/setter
- settings: PUT /preferences/wecom-bot 保存即重建连接 + 聚合 GET 补字段
- main.py: lifespan 挂载启停(失败不阻断应用启动)

前端:
- api.ts: Preferences 加 3 字段 + updateWecomBot 函数
- Monitoring.tsx: 智能机器人配置卡片(BotID + Secret 密码框 + 连接状态)

本阶段只做连接保活, 暂不处理消息收发(@交互/流式回复后续扩展)。

* fix(wecom-bot): 卡片标签改为「企业微信 智能机器人」与上方一致

* feat(wecom-bot): 智能机器人长连接独立开关

卡片头部加勾选框, 用户可明确控制长连接开启/关闭:
- 勾选 → 建立 WebSocket 连接保活
- 取消勾选 → 立即断开

后端: 新增 PUT /preferences/wecom-bot-toggle(只切 enabled 不改凭证)
前端: toggleWecomBot API + 勾选框 + 说明文字
凭证不齐时勾选框禁用(无法连接)

* feat(wecom-bot): 接收消息解析并记 INFO 日志(验证接收能力)

收到消息从 DEBUG 提升到 INFO, 并解析关键帧:
- aibot_msg_callback: 用户消息(会话类型/消息类型/用户ID/内容)
- aibot_event_callback: 事件回调(进入会话/卡片点击/被踢等)
- 其他帧: cmd + 原文截断

发消息给机器人即可在后端日志看到接收记录。
2026-07-09 16:35:21 +08:00
wshy c1600837b1 fix(overview): dashboard top indices now sync with realtime quotes (#84)
看板顶部四个指数在开启实时行情时不刷新, 但左侧菜单的指数能刷新。

根因: build_market_overview 在装配前会把 as_of 从 None 解析为真实日期
(latest_date), 导致 _index_quotes 内 `as_of is None` 判断永远为 False,
实时缓存分支走不到, 始终回退 kline_index_daily 数据库快照。

- market_overview_builder: 仅当调用方显式指定日期(历史复盘)时才让
  _index_quotes 回退数据库; "最新"请求传 None 优先读实时缓存
  (_index_quotes 自身有实时为空回退数据库的兜底, 盘后不受影响)
- quote_service: 实时刷新后清空 overview 聚合缓存, 使 SSE 触发的重取
  拿到最新值, 与侧栏 /intraday/indices (无缓存) 行为对齐
2026-07-09 13:49:41 +08:00
wshy ddde2b9653 feat: 数据修正功能 + 盘后管道暂停实时行情防竞态 (#87)
* refactor: 「群机器人 Webhook」统一更名为「群推送 Webhook」

"群机器人"易与后续接入的"智能机器人(API 模式)"混淆。
该通道本质是单向往群聊推送消息, 更名为「群推送 Webhook」更准确。

涉及: 飞书/企业微信的 UI 标签、操作指引、后端文档字符串、
错误提示文案(代码逻辑/接口不变)。覆盖 6 个文件, 纯文案改动。

* feat: 数据修正功能 + 盘后管道暂停实时行情防竞态

数据修正/补数据:
- 数据页顶部新增「修正数据」按钮, 弹窗选起始日期重拉到今天
- 复用盘后管道全流程 (维表/A股日K/除权/enriched/指数), 仅日期由用户传入
- run_now() 加 override_start_date 参数, 注入 A股日K + 指数拉取起点
- 新增 /api/kline/repair_daily 端点 (异步 job + 进度轮询)
- 前端 RepairDailyPanel + DatePicker, 默认起始日期为30天前

实时行情暂停机制 (防写盘竞态):
- QuoteService 加 _paused 标志 + pause()/resume()/paused() 上下文管理器
- 盘后管道/数据修正运行期间自动暂停实时行情取数, 防止覆写同一批 parquet
- toggle 端点: 暂停态下禁止开启实时行情 (409)
- 前端开关: 暂停时 disabled + 显示「数据同步运行中,已临时暂停」
- 三处注入 pause: pipeline.py / kline.py(repair_daily) / daily_pipeline.py(定时)
2026-07-09 13:47:39 +08:00
wshy 19b2fd8480 refactor: 「群机器人 Webhook」统一更名为「群推送 Webhook」 (#86)
"群机器人"易与后续接入的"智能机器人(API 模式)"混淆。
该通道本质是单向往群聊推送消息, 更名为「群推送 Webhook」更准确。

涉及: 飞书/企业微信的 UI 标签、操作指引、后端文档字符串、
错误提示文案(代码逻辑/接口不变)。覆盖 6 个文件, 纯文案改动。
2026-07-09 13:33:18 +08:00
wshy c822b4ff53 fix(wecom): 超长复盘消息截断后追加提示行并提量 (#85)
企业微信群机器人 markdown 上限 4096 字节(经官方文档核实), 复盘报告超长时
会被静默截断, 用户无从知晓还有更多内容。

- 截断阈值由 3800 提到 4000 字节, 在 4096 硬限内尽量多显示正文
- 超长截断时追加提示行「…内容较长已截断, 更多详情请回到 TickFlow 应用内查看」
- _truncate_to_bytes 支持自定义 suffix, 按字节为提示行预留空间
- 顺手修正企业微信字段名 msg_type→msgtype, 推送失败日志 debug→warning
  (此前企业微信推送失败静默吞掉, 日志里看不到)
2026-07-09 13:28:20 +08:00
shy3130 fbcfd31d6a feat: 优化策略创建流程 2026-07-08 22:17:11 +08:00
shy3130 aea694ee83 fix(strategy): keep AI-generated strategies in AI pool 2026-07-08 18:38:14 +08:00
wshy cf53953c8d fix(realtime): ensure final quote sync and badge init (#79) 2026-07-08 18:18:55 +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
wshy 5732155ec3 fix(indicators): 最新日涨跌停信号优先使用维表价格 (#77) 2026-07-08 16:54:00 +08:00
wshy f0a082b01a chore: 数据获取耗时诊断日志 + 指数卡片涨跌着色 (#76)
* chore(diag): 数据获取全链路加分段耗时日志, 定位 WSL 卡死

用户反馈 WSL 下数据获取超时 800s 疑似卡死。在三个可能卡住的环节
加轻量诊断日志(不改业务逻辑), 复现一次即可从日志定位根因:

- policy.py: 能力探测每个 cap 的开始/完成/耗时/结果 + 探测总耗时
  (启动期 13 次串行探测是首要嫌疑)
- client.py: 3 个 SDK 客户端创建时记录端点 + 标注 SDK 默认超时
  (timeout=30s, max_retries=3, 单次最坏 127s)
- quote_service.py: 全市场行情/核心指数拉取的条数 + 耗时
- bridge.py: stock-sdk 桥接 subprocess 的 op + 耗时 + 超时上下文

* style(sidebar): 指数卡片价格按涨跌着色, 与涨跌幅同色
2026-07-08 15:15:30 +08:00
xtxoandtokenming f273349cc8 fix: allow stock-sdk plugin on Windows (#75)
Co-authored-by: tokenming <xtxo@users.noreply.github.com>
2026-07-08 14:19:52 +08:00
wshy 8e56f3efb8 feat(monitor): 推送渠道拆分为飞书/企业微信独立勾选 (#74)
把监控推送从单一总开关 webhook_enabled(bool)升级为渠道数组
webhook_channels(list),飞书与企业微信各自独立勾选,做到「勾哪个
推哪个」。沿用项目已有的 review_push_channels 渠道数组模式。

后端:
- RuleModel 新增 webhook_channels 字段;normalize() 兼容老规则,
  webhook_enabled=True 自动迁移为 ['feishu','wecom']
- _maybe_send_webhook 按 channels 判断:'feishu' in channels 才推
  飞书,'wecom' in channels 才推企业微信
- 新增偏好 webhook_default_channels + PUT 接口;老 webhook_enabled_default
  保留兼容

前端:
- RuleEditor 飞书/企业微信勾选各自绑数组 includes/toggle
- 设置页推送通知卡片飞书/企业微信默认勾选独立
- 连板梯队封单弹窗「飞书」单胶囊改为飞书+企业微信双胶囊
- api.ts 类型 + updateWebhookDefaultChannels
2026-07-08 13:48:22 +08:00
wshy 5b2c88c93e fix(depth): 午间休市停止 sealed 轮询, 修复集合竞价盘口覆盖致涨停误判 (#73)
_poll_loop 原用宽窗口 _is_trading_hours() (9:25-11:35 / 12:55-15:05),
12:55-13:00 午后集合竞价准备期会恢复拉 depth. 此时 ask1/bid1 为竞价盘口,
语义不同于连续竞价, 「涨停价上卖一==0」的真封判定失效, 会用竞价盘口覆盖
11:30 已定格的正确 sealed 值, 导致涨停股误判为 sealed=False (假涨停) 被错误
扣减, 部分错误归入「炸板」, 连板梯队与涨跌停计数失真.

修复: depth sealed 轮询改用新增的 _is_continuous_trading()
(9:30-11:30 / 13:00-15:00, 仅工作日), 与 quote_service 同名方法窗口一致.
午间 11:30-13:00 停止轮询, _sealed_cache 定格在 11:30 的正确值供正常显示,
quote_service 的价格轮询保持宽窗口 (合理, 盘前预热/收盘捕捉) 不受影响.

is_sealed_ready 不改动: 数据已落盘/缓存, 午间正常连续运行时本就不会降级,
直接复用定格值即可.

新增 13 个测试覆盖连续竞价窗口边界 + 午间休市/集合竞价准备期/周末回归点,
并逐点校验与 quote_service._is_continuous_trading 一致.
2026-07-08 13:42:53 +08:00
wshy 0bc5ff1831 fix(strategy): 加载边界归一化 META params, 修复非标准格式导致 /api/strategies 500 (fixes #68) (#72)
custom/AI 策略的 META["params"] 可能是 dict / list[str] 等非标准格式 (LLM 偶发
漂移 / 用户手改). 加载时 _load_file 只 setdefault("params", []) 不校验格式,
坏策略照样进内存, 在 _strategy_detail() 的 {p["id"]: p["default"] for p in params}
处抛 TypeError, 一个坏策略拖垮整个 /api/strategies 列表 500. 回测链路
backtest/strategy.py:589 的 _normalize_params 同样会崩.

在 _load_file 加载边界新增 _normalize_param_defs(), 把任意格式统一成标准
list[dict]: dict 按 key 作 id 转换, list[str] 每项作 id, 不可识别项丢弃并 warning,
整体异常降级为空 list. 三类来源 (builtin/custom/ai) 都走 _load_file, 单点覆盖.
降级而非崩溃: 策略仍可见可用, 只是设置面板无参数滑块.

新增 11 个测试覆盖归一化各分支 + _strategy_detail 不再 500 的回归断言.
2026-07-08 12:34:08 +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
wshy 0121a5583f fix(strategy): 策略详情合并触发器 override, 修复买卖信号 tag 保存后回显丢失 (#71)
_strategy_detail 原直接返回策略源文件默认值 s.entry_signals / s.exit_signals,
没有像 stop_loss / max_hold_days 那样合并 overrides, 导致用户在策略卡片弹窗里
新选的买入/卖出触发器 tag 虽已保存进 strategy_overrides JSON, 但再次打开时
回显的还是源文件默认值.

保存落盘 / 回测 / 监控链路均正常 (它们各自有 _effective_signals 合并逻辑),
只有 GET /api/strategies/{id} 这条详情链路漏了 merge.

新增 4 个回归测试覆盖 _strategy_detail 的触发器合并契约.
2026-07-08 11:27:59 +08:00
wshy 3820b35921 fix(watchlist): 自选页 enriched 改为 LEFT JOIN, 修复不在缓存 universe 的自选股被静默丢弃 (#70)
后端 watchlist_enriched 原以 enriched 缓存为主表执行 inner filter
(df_e.filter(is_in(stock_symbols))), 方向反了: 不在缓存 universe 里的自选
标的 (新股/冷门股/新用户未同步) 被整行丢弃. 改为以自选列表为主表 LEFT JOIN
enriched, 缺失标的指标为 null, 前端已有 '—' 占位渲染兜底. ETF 分支同理.

前端 Watchlist 顶部胶囊区原 hiddenCount = allSymbols - sortedRows, 把 '数据
未返回' 误算为 '被筛选隐藏'. 拆分为两个口径:
- hiddenCount: rows.length - sortedRows.length (真正被筛选条件隐藏)
- pendingCount: sortedRows 中 close 为 null 的行数 (指标未就绪)
并新增 '待数据 N' 灰色提示与原 '已过滤 N' 区分.

新增 4 个回归测试覆盖核心契约.
2026-07-08 11:06:56 +08:00
wshy 5b2282f8cb feat(ext-data): 新增扩展数据支持 URL 创建并调整设置弹窗 tab (#64)
- 新建扩展数据弹窗加入 URL/文件/手动 三种接入方式, URL 模式支持探测字段、预览、保存拉取配置并可选立即导入或定时拉取
- 后端新增 /api/ext-data/detect-url 探测接口, 自动识别 JSON 行数组路径、字段类型与 symbol/code 候选列
- 抽出 infer_fields_from_df / detect_symbol_candidates / apply_config_mapping 公共逻辑, 文件上传与 URL 探测复用
- 拉取写入支持仅含 code 的数据, 按 code_map 自动生成标准 symbol
- 设置弹窗 tab 顺序调整为 拉取/推送/上传, 默认显示拉取
2026-07-07 18:04:49 +08:00
shy3130 f8914c9140 feat: 新增企业微信推送、个股分析页重构、价位算法优化及多处交互改进 (v0.1.82)
- 推送: 新增企业微信群机器人通道(与飞书并列), 监控告警/复盘报告多渠道分发
- 个股分析: 历史报告改为右侧常驻栏(展示全部股票报告), 进页面自动恢复上次选股
- 价位算法: 成交密集区改用换手率衰减模型(国内主流筹码分布); 缺口回补判定修正(必须完全穿越缺口)
- 回测: 回撤止盈改纯峰值口径(与 trailing_stop 一致); 佣金/印花税/滑点合并一行; 建仓口径加问号气泡说明
- 分时图: 自选列表迷你分时图加渐变填充(对齐个股对话框风格)
- 图表: 个股分析日K图右侧标签适配浅色主题; 价位标签与下方文字行双向 hover 高亮联动
2026-07-07 17:56:25 +08:00
shy3130 03e99bd05f feat(stock-sdk): 取数入口补充 INFO 日志, 便于确认切换后实际走 stock-sdk 2026-07-07 13:07:59 +08:00