Commit Graph
15 Commits
Author SHA1 Message Date
kevin9327 3f9c6da7dd fix(ext-data): 卡片「最新」时间按北京墙钟, 不再随服务端时区漂移
扩展数据页同一张卡片上两个时间对不上: 底部「最新」显示 10:30:00, 而设置
里拉取面板的「上次」显示 18:30 —— 说的是同一次同步。Docker 镜像默认 UTC,
差的正好是一个时区。

latest_sync_date 由 parquet mtime 经 datetime.fromtimestamp(mtime) 格式化,
拿的是宿主机时钟, 且不带时区后缀; 前端 ExtDataStatCard 原样展示这串裸时间,
没有任何换算余地。旁边的 pull.last_run/next_run 是 datetime.now(timezone.utc)
的带偏移 ISO, 前端 new Date(iso) 按浏览器时区渲染, 所以那一侧是对的。

快照与时序两条路径都改成 fromtimestamp(mtime, tz=CN_TZ), 与
json_report_store._now_iso「用北京墙钟而非宿主机时钟」同口径。
展示格式不变。
2026-09-10 08:19:30 +09:00
kevin9327andClaude Opus 5 68005c92e6 fix(alerts): 触发记录查询的 days/limit 补上范围约束
GET /api/alerts 的 days、limit 是裸 int,没有任何约束,越界值不报错而是
静默返回错误结果:

- days=-1  → cutoff 被推到未来,200 但 alerts 为空(total 仍是 3)
- limit=-1 → out[:limit] 变成负数切片,3 条记录只返回 2 条,
             静默丢掉最旧一条,调用方无从察觉
- limit=0  → 200 但 alerts 为空

同类列表端点已有正确写法:abnormal.py 用 `limit: int = Query(500, ge=1,
le=2000)`,rps.py 用 `days: int = Query(12, ge=7, le=30)`,越界直接 422。
本次把 /api/alerts 补齐到同一口径,上下限取存储侧保留策略本身
(alert_store.MAX_DAYS / MAX_RECORDS),不新增常量,超出保留窗口的请求
本来也没有可返回的数据。

前端 alertsList 实际只传 days=7、limit=1/10/500,全部落在新区间内;
新增的 tests/test_alerts_query_bounds.py 同时锁住这些合法取值仍返回 200
且记录条数不变。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 07:37:17 +09:00
kevin9327andClaude Opus 5 fd5d883c8f fix(screener): run_all 的 as_of 与 /custom、/preset 同口径校验
/api/screener/run_all 收的是无类型 dict,as_of 只在 isinstance(str) 时才
解析,其余类型原样透传,造成两个问题:

1. 非法字符串("not-a-date"、"2026-13-01")抛未捕获 ValueError → 500;
2. JSON 数字({"as_of": 2026})不进解析分支,直接被当作日期用下去。
   实测:{"as_of": 2026} 返回 200,响应 as_of="2026",
   strategy_cache.json 也写入 as_of="2026"({"as_of": 20260904} → "20260904")。
   其它入口写进同一份缓存的是 "2026-09-04",cached-summary /
   cached-result 都按 as_of 字符串比对,格式不一致的缓存条目永远失配。

同一份 as_of 在 /api/screener/custom 和 /api/screener/preset 上走
Pydantic 模型(CustomRequest / PresetRequest 的 `as_of: Optional[date]`),
非法字符串和数字都会被 422 拦下;只有 run_all 这一处漏了。本次把 run_all
补齐到同一口径:非字符串直接 400,非法字符串 400「日期格式错误」。

补 tests/test_screener_run_all_as_of.py,同时锁住合法 as_of 仍然照常执行、
缓存仍写 ISO 日期,以及不传 as_of 时回退到 latest_date()。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 07:36:21 +09:00
kevin9327andClaude Opus 5 d2931aaadb fix(backtest): SSE 端点非法 start/end 返回 400 而不是 500
strategy/stream、optimize/stream、walkforward/stream 三个端点直接裸调
date.fromisoformat(start/end),前端传入 "not-a-date"、"2026-13-01"、
"2026/09/04" 之类的值会抛未捕获 ValueError,客户端只拿到 500 和
Internal Server Error,看不到是哪个参数不合法。

仓库内同类入口已有正确写法:signals.py 的 /intraday/replay 把
date.fromisoformat 包在 try 里返回 400「日期格式错误」,mining.py 的
MiningRunRequest 用 field_validator 走 422。本次只把 backtest 这三处
补齐到同一口径,不改其它行为。

补 tests/test_backtest_stream_date_guard.py:三个端点 × 非法 start/end
断言 400;另外用服务端范围保护(backtest_range_guard)让事件流立即收尾,
断言合法日期照旧返回 200,证明不是把入口收窄。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 07:35:32 +09:00
kevin9327 252872e84f fix(financial): 公告日为空的旧行不再压过带公告日的新行
_merge_report_history 用 nulls_last=True 排序, 而聚合是逐列
drop_nulls().last() — "排在最后"等于"最权威", 于是 announce_date 为空的
旧行反而胜出, 产出 announce_date 是新公告、数值仍是修正前的矛盾行,
点时因子在公告日之后放出的是旧数。改为 nulls_last=False: 公告日未知视为
最旧, 与 docstring 承诺的"新同步行有值则覆盖旧值"一致。
2026-09-09 07:33:28 +09:00
kevin9327 4bbc7d07a3 fix(kline): 实时行情覆写当日日K分区时补写 quote_ts
sync_daily_by_quotes 手工拼 record 时漏了行情响应的 timestamp, 而
flush_live_daily 是整分区覆写, 会把 QuoteService 之前写入的 quote_ts 一并
抹掉。data_integrity 的判据全建立在 quote_ts 上, 分区因此从"盘中快照"
退化为"权威历史": 盘中触发同步后停机, 次日自检漏判, 停机时刻的
close/volume 永久留存并污染 lookback 指标。
2026-09-09 07:25:35 +09:00
kevin9327 a49c8b3b15 fix(factors): 删除因子被拒时定义已被删掉
DELETE /api/factors/custom/{id} 的 404 守卫把 store.delete_one 当成存在性
探测, 但它会真的 unlink 文件并返回 True。当定义在盘上却没进注册表时
(load_into_registry 对注册失败的定义只告警跳过, 如复合因子的成员已被强制
删除), get_factor 为 None, 短路求值会走到 delete_one, 定义随即被删除。

随后的引用检查再返回 409「该因子仍有引用, 拒绝删除」—— 接口声称拒绝, 定义
却已经没了; 用户改带 force=true 重试只会得到 404, 定义无法恢复。

把 fail-closed 的引用检查移到存在性判定之前, 保证任何拒绝路径都不改磁盘。
2026-09-09 07:20:16 +09: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
kevin9327 ea27376e03 fix(kline): 分钟K增量同步窗口统一按北京时区
sync_and_persist_minute 的起点取自本地分钟K的北京墙钟, 终点却用服务器
本地墙钟 datetime.now(); _datetime_to_ms 按服务器时区解释两者, UTC 容器上
起点反而晚于终点, time_segments 为空, 增量补拉一个请求都发不出去。

_latest_minute_datetime / _earliest_minute_datetime 返回值带上 CN_TZ,
now 改用 cn_now(), 与 fetch_minute_single 已有的时区纪律保持一致。
2026-09-09 07:15:02 +09:00
kevin9327 e349f2ba9b fix(settings): 端点测速在清单缓存未预热时 500
_endpoints_cache 初值是 {"ts": 0.0, "data": None}, "data" 键存在, 所以
dict.get("data", {}) 的默认值不生效, 返回的是 None。进程启动后到第一次
GET /api/settings/endpoints 之前, POST /api/settings/test_endpoint 不带
rounds 会在 None.get("testRounds", 5) 上抛 AttributeError → HTTP 500。

前端 endpoints 查询有 5 分钟 staleTime, 后端重启后用户再点测速正好落在
这个窗口: 前端不会重新拉清单, 后端缓存却已清空。

改为先 `or {}` 兜底再取 testRounds, 保持原有默认 5 轮语义。
2026-09-09 07:12:54 +09:00
kevin9327 a0925cb0c8 fix(reports): AI 报告 created_at 改用北京墙钟
三类 AI 报告(财务分析 / 个股分析 / 大盘复盘)共用的 JsonReportStore 用
datetime.now() 补 created_at, 取的是宿主机时钟。容器默认 UTC 时写入的是
UTC 墙钟, 前端 fmtRelative 再按浏览器本地时区解析这串 naive 时间, 刚生成
的报告被显示成「8 小时前」; stockAnalysisStore 的「今天是否已生成过报告」
判定(created_at 前 10 位 == 浏览器今天)在北京 00:00-08:00 也会误判。

改用 app.market_time.cn_now(), 保持原有 naive 秒精度格式不变。
2026-09-09 07:10:55 +09:00
kevin9327 2266ecc1d8 fix(rotation): 轮动信号缺失日按日期归位补齐
_compute_rotation_signals 把缺失日的 (999, 0.0) 占位一律追加到序列末尾,
而下游按 ranks[0]=最早日 / ranks[-1]=最新日 解读。概念在部分日期缺席时
(build_rps_rotation 已过滤掉当日无有效 avg_pct 的成员) 实际排名被压到左端、
占位落到右端, 只在最近几日上榜的新晋概念因此被判成退潮。

改为按日期归位补齐, 并补充 tests/test_concept_rotation_signals.py 覆盖
早期缺席 / 最近缺席两个方向。
2026-09-09 07:10:35 +09:00
kevin9327 9a9903cf69 fix(ext-data): 定时拉取的时间窗口与落盘日期改用北京时间 2026-09-08 19:56:03 +09:00
kevin9327 6876db1a86 perf(ext-data): CSV 编码转换改为分块进行, 峰值内存不再随文件线性增长 2026-09-07 08:10:38 +09:00
kevin9327 f2fac2e8f0 fix(ext-data): CSV 编码转换保留原始换行, 避免最后一列被推断为字符串 2026-09-07 08:07:02 +09:00