Commit Graph
105 Commits
Author SHA1 Message Date
shy3130 46ebcd89c8 fix(regime): 修复重算无产出并补全 beta 标签
后端
- /recompute 全量分支原本误调 compute_regime_incremental(增量补差),
  无缺口/stale 时直接返回 0 行; 改为用 earliest_enriched_date ~ 今天
  走 run_regime_batch + upsert 强制覆盖, 真正实现"全量重算"
- _scan_enriched_fallback 原本漏调 compute_limit_signals 的 instruments
  参数致慢路径必抛异常 → enriched 缓存未预热时拿不到含信号列数据 → 聚合
  返回空; 改用主管道同款 compute_all(df, instruments, historical_shares)
  一站式补算, 与 indicators/pipeline.py 对齐
- 新增 enriched_date_set / earliest_enriched_date 辅助函数(抽出复用)

前端
- Regime 重算按钮: 加"重算中…"文字反馈 + 完成 toast(区分有/无新数据) +
  catch 捕获错误(原异常被静默吞掉)
- 市场环境菜单加 beta 胶囊标签(对齐 Settings 数据源标签样式)

验证
- 后端 582 passed; 真实数据全量重算产出 986 行(2022-07-08 ~ 2026-07-31)
- 前端 tsc + pnpm build 通过
2026-08-02 12:22:48 +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
wshy 84bc6cb875 Merge pull request #145 from intfoo/feat/index-support
自选/监控/个股分析支持指数(asset_type="index")
2026-07-31 19:19:17 +08:00
wshy b59be65107 Merge pull request #143 from CJ0Hn/feat/watchlist-screenshot-import
fix: 自选截图导入优化
2026-07-31 19:11:12 +08:00
wshy 89bb60bdaf Merge pull request #133 from dunmin1980-ux/cursor/tickflow-pro-rate-limit-p0
TickFlow Pro: process-local 80% RPM safety + Phase 1 probe scaffold
2026-07-31 19:08:11 +08:00
intfoo 865e75fc8b fix(index): 修复 PR #46 四个阻断项
阻断项1: 前端 TypeScript 类型扩展
- MonitorRule.asset_type 加 'index' (api.ts:517)
- screenerStrategies 参数加 'index' (api.ts:1412)
- klineMinute 响应 asset_type 去重 (api.ts:1296)

阻断项2: 指数监控独立评估
- _evaluate_monitors 股票早期 return 降级为 stock_ready 标志
  仅跳过股票轮, ETF/指数轮独立判断数据新鲜度
- 纯指数行情/自选场景下指数规则可正常触发

阻断项3: 核心指数模式不截断分区
- _process_full_market_records 按 index_mode 条件分支:
  mode=all (完整 CN_Index) → flush 覆盖; mode=core (部分标的) → merge 不截断
- merge_live_enriched_asset 对 index 正确更新 _index_enriched_cache

阻断项4: Free 档额度分批
- _fetch_watchlist_quotes 用 resolve_limit + chunked 按 capability batch 上限分批
- 失败批次跳过不整轮退出, 已有股票实时刷新不受影响
- 复用进程级共享限速器 sleep_between_batches

测试: +7 测试覆盖 4 个阻断项核心场景
2026-07-28 21:13:10 +08:00
intfoo ed4355e0ea feat: 指数(asset_type=index)后端接入 — 数据路由/自选enriched/监控指数轮/隔离防污染
- 数据路由: get_name_map 合并指数维表; get_enriched_latest_asset("index") 缓存+flush/merge 分支; daily-batch 按资产分组
- 自选: watchlist_enriched 指数分支 + 行级 asset_type 标注
- 监控: MonitorRuleEngine 第三轮指数评估 (signal/price); 指数实时焐热复刻 ETF flush; Free档自选实时资产分流; 规则校验 (禁 strategy/market/ladder/分时信号)
- 隔离: _resolve_universe 过滤指数防污染股票日K/分钟K; 指数轮 reset_strategy_results=False; 策略/回测/screener 零改动
- AI 分析: prompt 指数无财务文案
2026-07-26 18:35:31 +08:00
shy3130 b43b177899 feat(monitor): add strategy signal event controls 2026-07-26 16:08:42 +08:00
CJohn e526d25fd0 fix(ocr): 完善截图导入的安全限制与交互状态
- 在 Pillow 完整解码前检查图片像素数,并将 DecompressionBombError
  转为明确的参数错误,避免压缩大图在校验前占用过多内存
- 为 OCR 线程任务设置独立的 AnyIO CapacityLimiter(2),最多同时执行
  两次图片解码与 Tesseract 识别,其余请求排队等待
- 前端调用 ocr-status 检查 Tesseract 是否可用;不可用时禁用截图导入
  入口,并按 Windows、macOS 和 Linux 显示对应安装说明
- 关闭弹窗或重新选择图片时取消旧请求,并通过请求代次校验忽略迟到
  的异步结果,防止旧候选回写到新弹窗
- 禁用已存在于自选列表中的候选项,批量添加接口返回实际净新增数量,
  成功提示使用后端返回值
- 补充图片类型与大小校验、解压炸弹、OCR 状态、并发限制以及批量添加
  数量等测试
2026-07-26 12:19:55 +08:00
intfoo 99b0c6cb35 fix(index-quotes): compute change_pct for custom data source
Custom data source realtime quotes were missing change_pct because _build_index_quotes only kept pre-existing columns. TickFlow path computes it in _fetch_full_market_quotes, but custom source path bypasses that computation.

- _build_index_quotes computes change_pct/change_amount from last_price/prev_close when not provided by the source
- guards prev_close=0 (Polars produces inf, invalid JSON)
- aligns with TickFlow path and _fallback_index_quotes_from_daily
2026-07-24 11:35:19 +08:00
shy3130 72918fe602 优化策略卡片结果加载性能 2026-07-20 13:01:03 +08:00
shy3130 4df3062002 fix: 完善自定义分钟数据源配置与异常处理
- 补齐资产类型和周期参数在 API、YAML 与前端编辑器中的保存回读

- 权限判断和分时监控统一复用安全的数据源解析边界

- 增加配置往返及解析异常回归测试
2026-07-19 17:01:33 +08:00
intfoo d9a77a534a fix(minute): 承接 #122 review 三处阻断问题整改 + resolver 异常边界加固 2026-07-19 16:00:22 +08:00
intfoo 4c85f99633 fix(minute): 自定义分钟数据源用户单股补拉/监控页整改 (承接 #121 review) 2026-07-19 13:35:35 +08:00
intfooandshy3130 e2ffde99d5 fix(minute,monitoring): 自定义数据源用户单股补拉/监控页打通 (#121)
承接 #126: 能力探测已补 KLINE_MINUTE_BATCH, 但两处取数/UI 仍按 TickFlow
档位拦截, 配了自定义分钟/实时源的 None 档用户依旧被挡。

- kline_sync.fetch_minute_single: 与 sync_minute_batch 一致, 先查
  preferences.get_minute_data_provider(), 非 tickflow 且自定义源有
  minute dataset 时走 custom provider, 避免分时图首次打开(本地无数据)
  补拉返回空。
- Monitoring.tsx: None 档但配了自定义实时源时, 后端
  is_realtime_allowed(realtime_mode=full_market) 仍返回 True。
  以 quoteStatus.realtime_allowed 作为最终判据, 不再用 isNoneTier
  一刀切拦截实时监控页。

Co-authored-by: shy3130 <shy3130@users.noreply.github.com>
2026-07-19 13:26:52 +08:00
dunmin1980andCursor 0745fdbb7d fix: apply process-local 80% TickFlow rpm safety budget
Scale resolve_limit rpm by SAFETY_RPM_FACTOR and document that the
in-process slot limiter is not a cross-container account budget.
Keep Stage A isolation via off-peak A-share usage guidance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-19 09:36:55 +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 a1e8be58db feat: release v0.1.86 2026-07-18 00:49:28 +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
ChenJunheng 0d6b341945 fix(watchlist): 采纳 #94 审查 — 防大图 OOM、OCR 不阻塞事件循环、收紧图片类型
为截图预处理增加像素上限与长边降采样;import-image 将 OCR 放入线程池;
去掉任意 image/* 放行,仅允许白名单 MIME/扩展名。
2026-07-15 23:28:30 +08:00
ChenJunheng 6f084e1b29 feat(watchlist): 支持从券商自选截图批量导入
基于 Tesseract OCR 识别六位代码并校验证券主数据,前端提供导入确认弹窗;
修复批量添加后 enriched 缓存未刷新导致需手动刷新页面的问题。
2026-07-15 23:25:46 +08:00
shy3130 f317377bef fix(strategy): apply saved params to builtin screeners 2026-07-15 20:06:11 +08:00
wshyandshy3130 70a4aad010 feat(monitor): 个股通知带行业/概念 ext 标签 + 全局配置 (#118)
监控中心顶部加全局配置(齿轮), 选择要在个股通知里展示的 ext 字段
(默认概念 ext_gn_ths + 行业 ext_hy_ths)。开启后触发记录卡片和推送
toast 统一显示行业(蓝)/概念(橙)标签。

架构: 源头富化 — 后端 SSE 推送前 (_enrich_alerts_ext) 用
_load_ext_value_maps (带 mtime 缓存) 按 symbol 富化; GET /api/alerts
读取时同样富化。前端零额外请求。

配置支持细粒度裁剪:
- 字段下拉按扩展表分组 (optgroup)
- 显示前N个 (maxTags), 留空=全部
- 隐藏指定位置 (hiddenIndices), 点数字划掉

数据结构 {field, maxTags, hiddenIndices} 向后兼容旧字符串格式。

后端:
- preferences.py: get_monitor_ext_fields + _normalize_ext_field
- settings.py: RealtimeMonitorConfigIn + get_preferences 加字段
- quote_service.py: _enrich_alerts_ext SSE 推送前富化
- alerts.py: list_alerts 加 ext_columns 参数

前端:
- api.ts: MonitorExtFieldItem 类型 + alertsList extColumns 参数
- Monitor.tsx: 齿轮入口 + 配置弹窗 + 卡片标签行
- AlertToast.tsx: toast 底行加标签

Co-authored-by: shy3130 <shy3130@users.noreply.github.com>
2026-07-14 18:49:23 +08:00
wshy 644cf8648d Docker 环境支持 Codex CLI (#116)
解决 Docker 容器内无法读取 codex 命令的问题:多阶段构建提取原生二进制 + 只读挂载主机登录态 + 适配 loopback local-access provider。
2026-07-14 18:07:38 +08:00
wshyandshy3130 d8c2081679 fix(quote): 最后获取时间持久化, 关闭开关/重启后仍显示 (#115)
_fetched_at 原为纯内存变量 (init 0.0), 进程重启即归零 → last_fetch_ms
变 null → 数据页"最后获取"显示"—"。用户关闭实时开关若伴随重启
(开发热重载/部署), 就看不到上次拉取时刻。

改为持久化到 preferences.json:
- __init__ 启动时读回 last_fetch_ms 作为 _fetched_at 初始值
- _fetch_quotes 成功后 (全市场 + 自选股两处) 写入 preferences
- 放锁外调用, 失败不影响主流程 (内存值已更新)

前端 QuoteConfigCard 本就无条件渲染该行, 无需改动。

Co-authored-by: shy3130 <shy3130@users.noreply.github.com>
2026-07-14 16:46:23 +08:00
wshyandshy3130 d99112c160 分时图实时刷新: 日期修复 + 间隔可配 + 对话框实时 + 自选刷新图标 (#114)
* fix(kline): 分时图盘中显示昨天而非今天

get_minute_batch 的日期选择存在死循环: trade_date=date.today() 后立即
if trade_date==date.today() (恒真), 无条件用 latest_minute_date_global()
覆盖今天; 但批量实时补拉不落库, 该值盘中恒返回昨天 -> 永远回退到昨天 ->
expected=240 判定昨日完整 -> 不触发今天补拉。

改为仅在真正非交易日回退 (周末 + 收盘后无今日日K 的节假日), 盘中保持
今天让 completeness 检查触发实时补拉。同时全量 date.today()/datetime.now()
改用北京时间 cn_today()/cn_now(), 与 quote_service/monitor 等一致。

影响: 自选列表分时列 + 单股详情分时图。

* feat(settings): 分时图刷新间隔可配置 (默认6s, 范围3-60s)

原写死 15s 轮询, 现可在 实时监控设置 -> 分时图刷新 卡片用滑块调节。
复用 minute_intraday_refresh 偏好链路, 新增 minute_intraday_refresh_interval
字段 (preferences + settings API + 前端 Preferences 接口)。

- preferences.py: getter 默认6s clamp[3,60] + set/get config 接入
- settings.py: RealtimeMonitorConfigIn 字段 + get_preferences 返回
- Monitoring.tsx: 滑块控件 (参考行情轮询滑块, 2s 防抖保存)
- Watchlist/Screener: 15_000 -> interval*1000, fallback ?? 6

* feat(dialog): 个股对话框日K/分时实时刷新

日K走 SSE 精准刷新: 对话框打开时注册焦点股票, quotes_updated 推送时
invalidate ['kline', symbol], 后端 _maybe_inject_live_candle 只读内存
不调 TickFlow, 秒级零成本。关闭/切股自动注销焦点。

分时走轮询: 复用分时刷新开关 + 间隔偏好, 经 StockPanel.refetchIntervalMs
透传到 StockIntradayChart。分时端点数据不完整会调 TickFlow, 不接 SSE
避免打爆限流。

不影响回测弹窗 (不注册焦点、不传间隔)。

* feat(watchlist): 分时列表头加刷新图标 (与策略列表一致)

分时列可见 + 未自动刷新时显示手动刷新按钮 (点击 refetch, 加载旋转);
自动刷新中显示持续旋转小图标提示。逻辑/样式对齐 ScreenerTable。

* fix(badge): 真假涨停修正弹层被父级裁剪遮挡

弹出层原用 absolute 定位, 被看板卡片父级的 overflow-hidden +
backdrop-blur containing block 裁剪/遮挡。改用 createPortal 渲染到 body,
基于徽章 getBoundingClientRect 计算弹层坐标, 脱离父级裁剪。

---------

Co-authored-by: shy3130 <shy3130@users.noreply.github.com>
2026-07-14 15:46:18 +08:00
zhang,zhao 46338f4435 修复:适配 Docker Codex 本地访问服务 2026-07-14 11:51:35 +08:00
shy3130 590de5d694 fix(monitor): 分时图刷新开关不生效 + Pro+ 默认开启 + 刷新条件统一
三处问题:

1. 后端 API 漏接线 (settings.py):
   - 入参模型 RealtimeMonitorConfigIn 缺 minute_intraday_refresh →
     PUT 时 Pydantic 静默丢弃, 开关拨了也不存
   - GET /preferences 响应没返回该字段 → 前端永远读 undefined→false,
     开关拨完弹回 OFF
   服务层 (preferences.py) 本就有 get/set, 纯 API 层漏接。

2. 默认值 (preferences.py):
   get_minute_intraday_refresh 原默认 False。改为三态: 未设置过的
   key 按 Pro+ 权限定默认 (有权限=开, 无=关); 用户主动设置过的
   (含 False) 以用户选择为准。用 'in data' 区分 未设置 vs 设为 False。

3. 刷新条件不一致 (Watchlist.tsx):
   Watchlist 用 || (开关或实时行情任一即刷新), Screener 用 && (两者都要)。
   与设置卡片描述「需 Pro+ 权限 + 实时行情运行」对齐, 统一成 &&。

后端 216 测试通过; 前端 tsc 通过。
2026-07-13 22:28:04 +08:00
shy3130 570bcba8b9 fix(ext-data): List→string 字段直接 cast 报错 — 先 join 再 cast
手动创建的扩展配置拉取 THS 概念接口时, concepts 列为 List(String),
声明为 string 后 cast_df_to_schema 直接 cast(pl.Utf8) 抛
「cannot cast List type (inner: String, to: String)」。

cast_df_to_schema 检测到源列是 List 且目标为 Utf8 时, 先把元素
转字符串再以分号拼接 (与 _flatten_concept_rows 一致), 然后再 cast。
保护任意返回数组字段的配置, 不再依赖预设 id 走 flatten 分支。
2026-07-13 18:39:10 +08:00
wshyandshy3130 697ff2a181 fix(compliance): reinforce research-only boundaries (#108)
Co-authored-by: shy3130 <shy3130@users.noreply.github.com>
2026-07-12 22:42:38 +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 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
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 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
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
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
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