Commit Graph
6 Commits
Author SHA1 Message Date
kevin9327 d46e601863 fix(notifications): 飞书推送按传入的 max_attempts 退避与记日志
_post_feishu 的退避判断与最终 WARNING 仍用模块常量 _FEISHU_MAX_ATTEMPTS(3),
设置页「发送测试消息」传 max_attempts=1 时: 唯一一次失败后仍 sleep 1 秒才返回,
日志写「已重试 3 次」而实际只试了 1 次。两处改用 max_attempts; 生产路径不变。

测试: 单次尝试不退避且日志计数为 1 (未修复时失败), 默认 3 次仍退避 1s、2s。
2026-09-05 21:14:53 +09:00
kevin9327 e12e0c7d02 fix(watchlist): 自选导入上传分块读取, 越限即拒绝而非读完再拒
import-csv / import-image 原先 `await file.read()` 之后才比较长度, 上限只在
整个文件进入内存之后生效, 与 issue #204 修复前的扩展数据上传同类。新增
_read_upload_capped 分块读取, 累计超过上限的那一块立即返回 400 并停止读取,
状态码与文案不变; 内存占用不超过上限 + 一块。

测试: 新增 5 例 (含「越限后不再继续读取」的字节计数断言); test_watchlist_csv
的上传 mock 改为像真实 UploadFile 一样读尽返回 b""。
2026-09-05 21:11:44 +09:00
kevin9327andClaude Opus 4.8 ebf1a89254 fix(ext-data): 扩展数据上传增加体积上限并分块落盘 (#204)
问题: 扩展数据上传 /api/ext-data/{id}/upload 与字段检测 /api/ext-data/detect-fields
都直接 `content = await file.read()`, 无任何字节/行数限制。上传超大 CSV/XLSX
会把整个文件读入内存再交给 Polars/Excel 解析, 可能造成高内存占用、进程 OOM 或
服务不可用。自选截图 OCR 已有 12MB 上限, 扩展上传却没有统一上限。

修复: 抽出 _write_upload_capped(file, dest, max_bytes), 以 1MB 分块把上传写入
临时文件, 累计超过上限即拒绝(413), 不再一次性读入内存; 两个上传入口统一改用它。
上限常量 _MAX_UPLOAD_BYTES = 50MB(对扩展快照数据足够宽裕, 亦便于按需调整)。

兼容性: 后缀白名单、解析与映射逻辑不变; 仅新增体积上限。413 由 detect_fields 的
`except HTTPException: raise` 与 upload_data 的 finally 清理临时目录正常传播。

验证: 新增 tests/test_ext_upload_size_limit.py 覆盖未超限完整写入、恰好等于上限、
超限抛 413 三例, 全过; ruff 无新增告警(B008 与 origin/main 同为 2 个, 均为既有
FastAPI File(...) 默认值写法)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 18:51:24 +09:00
kevin9327andClaude Opus 4.8 d92ef17851 fix(backtest): max_hold_days 强制退出信号真正生效 (#198)
问题: /api/backtest/run 设置 max_hold_days 时, 强制平仓完全不生效, 持仓不会
按最大持有天数退出, 导致交易记录、持仓周期和收益统计失真。

根因(services/backtest.py 的 max_hold 内联逻辑, 两处缺陷叠加):
1. `exits_idx.iloc[end_i][col] = True` 是链式索引: iloc[end_i] 先取出一行副本,
   再对副本赋值, 在 pandas Copy-on-Write 语义下写入不会落到原矩阵(pandas 3.x
   直接抛 ChainedAssignmentError)。强制退出单元格始终为 False。
2. `exits_idx = entries.copy()` 以入场矩阵起步, 使强制退出矩阵天然带上所有入场位;
   即便修好第 1 点, 也会在入场当日就强制平仓, 而非 max_hold_days 之后。

修复: 抽出纯函数 _build_max_hold_exits(entries, max_hold_days), 从全 False 起步,
用单步定位 iloc[row, col_loc] 写入入场后第 max_hold_days 个交易日的强制退出位;
run() 中改为与用户 exits 做 OR, 保留原有信号退出。抽出后该逻辑不依赖 vectorbt,
可独立回归(run() 整体仍需 vectorbt 可选依赖)。

边界: end_i 越界 clamp 到最后一根 K; 入场即最后一根 K 时 end_i==i 不产生退出。

验证: 新增 tests/backtest/test_max_hold_exits.py 覆盖强制退出落位、不误标入场位、
越界 clamp、末根不退出、多入场、多列独立 6 例, 修复后全过; 并以复刻旧内联逻辑的
脚本在 pandas 3.0.5 上确认修复前目标退出单元格恒为 False(ChainedAssignmentError)。
ruff 通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 18:47:08 +09:00
kevin9327andClaude Opus 4.8 8aa6e8d89b fix(backtest): 因子回测默认区间改用 180 天而非 3 年 (#202)
问题: `/api/backtest/factor/run` 与 `/api/backtest/factor/batch` 省略 start
时, 使用了策略默认区间 STRATEGY_DEFAULT_DAYS(3 年), 而非为因子回测定义的
FACTOR_DEFAULT_DAYS(180 天)。以 end=2026-08-24 为例, 会解析成 2023-08-25。

影响: 用户只选因子直接运行会加载约 3 年数据, 徒增内存与耗时; 也与前端
「默认最近 3 个月」的提示不一致。且服务器区间守卫上限为 186 天, 该默认区间
在开启守卫时会被直接拒绝。

根因: factor_run / factor_batch 调用 _resolve_start 时误传 STRATEGY_DEFAULT_DAYS。
同文件的 strategy_run 已正确使用 FACTOR_DEFAULT_DAYS, 仅两个因子入口写反。

修复: 两处改为 FACTOR_DEFAULT_DAYS。显式传 start=null(全部历史)与显式日期的
语义保持不变(由 _resolve_start 的 model_fields_set 判定, 未改动)。

验证: 新增 tests/backtest/test_factor_default_range.py, 用替身 Service 捕获
传入配置, 覆盖省略 start、显式 null、显式日期三种情况(factor_run + factor_batch)。
修复前 2 例失败(解析为 2023-08-25), 修复后 4 例全过; 相邻 factor/optimizer
回归 28 passed; ruff 通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 18:42:32 +09:00
kevin9327andClaude Opus 4.8 64ab9a0818 fix(pipeline): A 股盘后拉取开关生效 (#216)
问题: 在设置里取消勾选「A 股」拉取(pipeline_pull_a_share=false)后完全不
生效, 每次同步仍拉取全市场 A 股日K。

根因: 两处联合导致开关不可用:
1. get_pipeline_pull_a_share() 硬编码 return True, 从不读取偏好, 使
   jobs/daily_pipeline.py 里 `if not pull_a_share` 的跳过分支永不可达。
2. set_pipeline_pull_types() 的白名单 _PIPELINE_PULL_KEYS 遗漏
   pipeline_pull_a_share, 即使 API 模型接受该字段, 值也会被静默丢弃、无法落盘。
   (ETF / 指数两个开关均正常读写, 仅 A 股异常。)

修复:
- get_pipeline_pull_a_share() 改为 load().get("pipeline_pull_a_share", True),
  与 ETF / 指数保持一致, 默认仍为 True (向后兼容, 旧配置无此键时行为不变)。
- 白名单补入 pipeline_pull_a_share, 使 PUT /api/settings/preferences/
  pipeline-pull-types 能正确落盘。

验证: backend/tests/test_pipeline_pull_types.py 覆盖默认值、关闭后 getter 生效、
setter 落盘与三开关独立性; 修复前 3 例失败, 修复后全过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 18:38:24 +09:00