Skip to content

DDB 后端系统性优化:bug 修复、并发安全、健壮性与查询性能(面向社区版 2核/8GB) - #3

Open
hugo2046 wants to merge 21 commits into
mainfrom
optimize/ddb-backend
Open

DDB 后端系统性优化:bug 修复、并发安全、健壮性与查询性能(面向社区版 2核/8GB)#3
hugo2046 wants to merge 21 commits into
mainfrom
optimize/ddb-backend

Conversation

@hugo2046

@hugo2046 hugo2046 commented Jul 21, 2026

Copy link
Copy Markdown
Owner

概述

optimize/ddb-backend 分支上对 DolphinDB 后端做一轮系统性优化,共 19 个原子提交。硬前提:目标服务器为 DolphinDB 社区版(2 核 / 8GB)——优化方向是省往返、省传输、省服务器内存、控会话数,明确不堆并行。D.features()/D.calendar()/D.instruments() 等公共接口的签名与返回结果完全兼容

变更内容

Bug 修复(5 项,全部带回归测试)

  • ddb_dataset_processor 传入 inst_processors 时提前 return pd.DataFrame(),并行处理结果被整体丢弃(data.py:782
  • DDBClient 连接池生命周期双重损坏:_pool_instance 类变量致多客户端共享同一池;close_pool 引用不存在的 _pool_lock 且永远读到 None(从未真正关闭过池);删除调用不存在 get_session() 的死方法与 __main__ 硬编码凭据
  • DolphinDBClientProvider init 急切创建 4 连接池(无消费方)→ 懒创建
  • load_mysql_plugin 误装 lgbm 插件(应为 mysql
  • DolphinDBDataLoader.__exit__/__del__ 无条件关闭进程级共享 session → _owns_session 所有权标志

并发/线程安全

  • 新增会话级 DBClient.session_lock(RLock):feature 查询是「run→upload→run」多步会话对话,并发交错会互相覆盖服务器端变量(跨线程数据污染根因);所有 session 触点统一持锁
  • QlibDataLoader 全局 _load_lock 收窄至 DDB 路径,文件后端恢复无锁并行

健壮性/质量

  • 12 处 print → loguru;3 处裸 except: 收敛;异常重包装补 from e
  • MySQL 同步 SQL 参数白名单校验(唯一真实用户可控注入面)
  • 清理死代码(注释块 ×2、零调用者函数 ×2、16KB 备份脚本)与 OPERATOR_MAPPING 重复键(生效映射不变,ast 测试锁定)

性能(计算分支每批 RPC 3-4 次 → 预热后 2 次)

  • 日历缓存:Alpha158 一次 D.features 从 ~6 次全量日历下载 → 0;DBCalendarStorage.index()/__getitem__ 改走 H["c"]
  • 往返缩减:日期字面量内联、纯字段分支单脚本查询、上传字典按分支裁剪;fetch_features_from_ddb 拆为编排器 + 4 个 helper
  • 多级缓存:existsTable 仅正向缓存、股票池走 H["i"]、表 schema 进程内缓存、表达式翻译 lru_cache;统一由 invalidate_ddb_caches() 失效(写路径自动调用)
  • 结果直构:计算分支直构 (instrument, datetime) MultiIndex 面板,替代 concat→unstack→stack→swaplevel 的 ~4 次全景拷贝(保留 legacy 兜底 + 逐值等价测试)
  • 惰性加载:三个 alpha 因子库(~119KB)按字段前缀按需加载,"Cannot recognize the token" 兜底重试,ddb_preload_alpha_libs 逃生开关
  • 写路径:按 URI 复用共享 DDBClient(批量导入 N 会话 → 1)
  • 配置化ddb_field_chunk_size=30ddb_days_step=252(默认值不变,附 8GB 内存模型说明与 live 基准脚本)

明确不做(2 核/8GB 约束)

读路径不引入 DBConnectionPool 并发、不调高 mr 并行度、不做服务器端全景 pivot、不引入 module/functionView 持久化。

追加(2026-07-22,ab525e91):滚动算子自动取前序期 + 真实分段计算

  • 取前序期:DDB 分支此前把 start_time 原样下发,Mean($close,20) 在区间头部前 19 个交易日必然 NaN。现在 Python 侧复用 qlib 算子树 get_extended_window_size 解析每批表达式的(向前, 向后)外扩交易日数(嵌套递归、双臂取 max、Ref($close,-2) 标签向后外扩),经 FeatureEngineeringByDate 尾参下发;DDB 端外扩查询窗口计算后截断回请求区间。qlib 解析不了的表达式退回正则扫窗口 + ddb_lookback_default(默认 252)兜底
  • 真实分段:移除从未生效的 repartitionDS(RANGE,[start,end])+mr(2 个边界只产生 1 个数据源的"虚假分区")。改为内存估算放得下整段单次计算;放不下按 ddb_days_step 切段顺序循环,每段带 lookback 重叠(halo)、段内截断、统一列标签对齐后 concatMatrix 纵向合并——分段边界处滚动结果与整段一致
  • 兼容:FeatureEngineeringByDate 新尾参默认 0,旧调用不变;D.features() 签名不变。离线套件 +4 项至 119 项全部通过(新增 tests/test_ddb_lookback.py);DDB 端脚本行为列入 live 验证清单(见 docs/DDB取前序期与分段计算_20260722.md

测试

  • 离线套件(mock 会话,不依赖 DDB 服务器):119 项全部通过(2026-07-22 起,含回看窗口解析与脚本接线测试);新增 tests/ddb_mocks.py 可复用记录型假会话与 RPC 计数基线
  • 行为等价保障:D0 先固化 fetch_features_from_ddb 四大分支行为,再做重构;直构重塑与 legacy 路径逐值等价测试(乱序轴/NaN/单 alias 等边界)
  • Live 验证待做(需 DDB 服务器):DDB_BENCH_URI=... python scripts/benchmark_ddb_backend.py 前后对比 + spans 成分股回测样例比对

🤖 Generated with Claude Code

hugo2046 added 19 commits July 21, 2026 16:49
- 删除 inst_processors 分支内提前 return pd.DataFrame() 的历史 bug(处理结果被整体丢弃且跳过归一化)
- 并行处理结果 concat 后 sort_index 保证确定性,落入统一的 cache_to_origin_data 路径
- 新增 tests/test_ddb_dataset_processor.py 离线回归(注入假 ExpressionD 与顺序 ParallelExt,覆盖分块与组合路径)

Co-Authored-By: Hugo <shen.lan123@gmail.com>
- 连接池由类变量改为实例属性:类变量会导致连接不同服务器的客户端共享同一个池
- close_pool 曾引用不存在的 cls._pool_lock(AttributeError 被裸 except 吞掉)且永远读到 None,实际从未关闭过池;改为实例方法 + 实例锁的双重检查
- 删除死方法 tableAppender/tableUpsert(调用不存在的 self.get_session(),全仓零调用者;正确实现在 DDBTableOperator)
- 删除 __main__ 块中的硬编码服务器凭据
- 新增显式 close();日志统一走 get_module_logger
- 新增 tests/test_ddb_client.py 离线单元测试(fake dolphindb SDK)

Co-Authored-By: Hugo <shen.lan123@gmail.com>
init 时急切执行 connector.pool 会向服务器开 4 条无人使用的连接,
在社区版(2 核/8GB)上白占服务器内存。改为惰性 @Property,
名称与类型保持兼容(DBClient.pool 首次访问时才建池)。

Co-Authored-By: Hugo <shen.lan123@gmail.com>
插件名笔误导致 MySQL 桥接的 mysql::connect 必然失败。
新增 tests/test_ddb_mysql_bridge.py 离线回归。

Co-Authored-By: Hugo <shen.lan123@gmail.com>
__exit__/__del__ 曾无条件 close 进程全局的 DBClient.session——
一个 with 块或一次 GC 就会把共享会话关掉,破坏 storage 与
feature 路径的所有其他使用方。引入 _owns_session 所有权标志,
仅独占会话时才允许关闭;__del__ 对构造中途失败也安全。

Co-Authored-By: Hugo <shen.lan123@gmail.com>
ddb.Session 非线程安全,且 feature 查询是「run(上传日期)→upload(变量)→
run(主查询)」的多步会话对话,并发交错会互相覆盖服务器端变量(跨线程
数据污染根因)。在 DolphinDBClientProvider 上新增 session_lock(RLock,
storage 读取可嵌套于 feature 查询持锁期间),并约定所有 DBClient.session
触点必须持锁:

- dolphindb_storage: exists/_read_calendar/_read_instrument/DBFeatureStorage.__getitem__
- loader.DolphinDBDataLoader: _validate_table_exists/_load_via_dolphindb/_load_from_dolphindb/get_table_info

新增 tests/test_ddb_concurrency.py:多线程交错检测 + RLock 可重入 + 接口契约。

Co-Authored-By: Hugo <shen.lan123@gmail.com>
跨线程数据污染仅在 DDB 后端出现(单一共享 session + 全局 H 缓存);
文件后端本就线程安全,全局锁对其是纯粹的吞吐回归。收窄后文件路径
恢复无锁并行;DDB 路径在会话级 session_lock 之外保留 load 级锁,
作为全局 H MemCache 写模式的双保险。

Co-Authored-By: Hugo <shen.lan123@gmail.com>
- 12 处 print 改为 get_module_logger(bridge 同步进度/警告、operator 建表提示、
  ddb_features 因子计算失败的排查上下文——instruments 只记数量与头部样本)
- 2 处裸 except: 收敛为 except Exception 并记录 warning(bridge 关闭路径)
- 3 处异常重包装补 from e 保留原始异常链(ddb_features 因子计算失败、
  loader 表校验/加载失败),异常类型不变

Co-Authored-By: Hugo <shen.lan123@gmail.com>
QlibDDBMySQLInitializer 的同步方法把用户可控参数直接 f-string 拼入 SQL
(唯一真实注入面)。新增 utils.validate_date_str(^\\d{8}$)与
validate_sql_identifier(^[A-Za-z0-9._-]+$,覆盖合法 Wind 代码)并应用于
sync_calendar/sync_feature_daily/sync_index_daily。合法输入行为不变,
病态输入在构造 SQL 前抛 ValueError。

Co-Authored-By: Hugo <shen.lan123@gmail.com>
- 删除注释掉的大块死代码 ddb_compute_features/get_query_date_range
- 删除零调用者函数 extract_fields_from_expressions/is_pure_fields_expressions
  (逻辑早已内联进 normalize_fields_to_ddb,未在 __init__ 导出)
- OPERATOR_MAPPING 删除被静默覆盖的前置重复键 Slope:mslr/Resi:mmse
  (ops.dos 自定义版本才是生效行为,语义不变)
- 删除从不加载的 ddb_scripts/featureEngineeringStreaming_bak(16KB 备份文件)
- 新增 tests/test_operator_mapping.py:锁定生效映射 + ast 检查禁止重复键

Co-Authored-By: Hugo <shen.lan123@gmail.com>
- tests/ddb_mocks.py:可复用的记录型假会话(run/upload/runFile/existsTable/
  loadTable 链式查询 + RPC 计数),供后续所有性能测试共用
- tests/test_fetch_features.py:固化四大分支行为(纯字段×list/dict、
  计算×list/dict)、空输入早退、spans 掩码、缺失字段兜底,以及当前
  RPC 往返数基线(日历每次全量下载、计算分支 2 次 run 等)
- scripts/benchmark_ddb_backend.py:DDB_BENCH_URI 开启的 live 基准
  (计时 + RPC 计数),离线 CI 安全跳过

Co-Authored-By: Hugo <shen.lan123@gmail.com>
- TradeDateUtils 增加模块级日历缓存:fetch_features_from_ddb 每批字段
  (30 个/批)都会构造本类,此前每次都全量下载交易日历——Alpha158 一次
  D.features ≈ 6 次下载,现在预热后为 0
- DBCalendarStorage.index()/__getitem__ 改走 H["c"] 缓存(此前绕过缓存,
  数据集对齐期间反复全量下载),新增 _cached_calendar() 与 data 共用缓存键
- 新增 ddb_qlib.invalidate_ddb_caches():清空日历缓存与 H["c"],
  write_df_to_ddb / clean_qlib_db(含 CSV 导入路径)写入后自动调用
- 新增 tests/test_ddb_calendar_cache.py;更新 RPC 基线测试为缓存后目标值

Co-Authored-By: Hugo <shen.lan123@gmail.com>
每批字段的 RPC 往返从 3-4 次降到 2 次(预热后):
- 日期改为字面量内联进查询脚本,消除独立的 start_time/end_time
  变量上传往返(复杂值 instruments/expressions 仍走变量上传,
  遵循 dolphindb_skill 的变量上传建议)
- 纯字段分支改为单条 SQL 脚本:spans dict 的 createDateStockMapping
  与 conditionalFilter 主查询合并为一次 run,同时消除 loadTable 句柄往返
- 上传字典按分支裁剪:纯字段分支只传 instruments

结构化拆分(行为不变,测试为证):200 行函数拆为编排器 + 4 个 helper
(_resolve_query_window/_fetch_pure_fields/_compute_expressions/
_format_result_panel),后续优化各自独立演进。

Co-Authored-By: Hugo <shen.lan123@gmail.com>
- existsTable 仅正向缓存(DBStorageMixin._exists_cache):每个存储访问器
  的 check() 此前都多付一次 RPC;表不存在仍持续报错(负结果不缓存)
- DBInstrumentStorage 股票池经 H["i"] 缓存(此前每次访问全量下载 +
  行循环重建 dict),返回浅拷贝防调用方污染缓存
- 新增 utils.get_table_columns 表列名进程内缓存,应用于 build_field_expr
  与 DDBTableOperator.table_appender/table_upsert(表结构仅随 DDL 变更)
- adapt_qlib_expr_syntax_for_ddb 在默认 OPERATOR_MAPPING 下经 lru_cache
  记忆化(identity 判断覆盖全部真实调用方,自定义映射直接解析)
- invalidate_ddb_caches 统一失效上述缓存;新增 tests/test_ddb_storage_caches.py

Co-Authored-By: Hugo <shen.lan123@gmail.com>
旧路径 concat→unstack→stack→swaplevel→sort_index 约 4 次全景拷贝
(5000 股×多年×30 列时数百 MB 客户端内存 churn),是客户端 CPU 主要
开销。_computed_dict_to_panel 按 (instrument, datetime) 预排序两轴后
一次分配直构面板;所有 alias 共享同一 (dates, codes) 轴(服务器端同一
panel() 产出),形状不一致(DDB 端异常占位返回)时运行时回退
_legacy_reshape 保持旧语义。pandas 版本分支随之移出热路径。

新增 tests/test_ddb_reshape_equivalence.py:乱序轴/NaN/单 alias/单日期/
单代码等形态下直构与 legacy 逐值等价 + 回退路径验证。

Co-Authored-By: Hugo <shen.lan123@gmail.com>
三个 alpha 库脚本(gtja191 56KB / wq101 40KB / qlib158 23KB,合计约
119KB)此前每次 qlib.init 都全量 runFile,让 2 核/8GB 社区版服务器
解析大量通常用不到的脚本。改为:

- init 仅加载核心三件套(ops/featureEngineering/prepareInstruments,
  ops.dos 优先序保持不变)
- ensure_alpha_libs_loaded 按字段前缀(gtjaAlpha/qlib158Alpha/WQAlpha,
  大小写不敏感)在 fetch 时按需加载,每会话每库仅一次
- 兜底:主查询报 "Cannot recognize the token" 时全量加载后重试一次
  (覆盖绕过字段扫描的调用路径),其他错误不重试
- 逃生开关:preload_alpha_libs=True 或配置 C["ddb_preload_alpha_libs"]
  恢复历史全量加载行为

Co-Authored-By: Hugo <shen.lan123@gmail.com>
create_table/clean_qlib_db/write_df_to_ddb(含三个 CSV 导入器路径)
此前每次调用都新建 DDBClient——新 TCP 会话 + 登录,批量导入 N 个
DataFrame 就开 N 条会话,白占社区版(2 核/8GB)服务器内存。

- 新增模块级 get_shared_client(uri):按 URI 复用进程内客户端
- 三个入口函数增加 keyword-only 参数 client=None(签名向后兼容),
  显式传入优先,缺省走共享客户端
- 共享客户端仅用于现状单线程写入流程(ddb.Session 非线程安全,已注释)

Co-Authored-By: Hugo <shen.lan123@gmail.com>
不猜新常数,把两个批次参数暴露为配置、由 live 基准按实际服务器校准:
- C["ddb_field_chunk_size"]=30:ddb_dataset_processor 每批字段数
- C["ddb_days_step"]=252:FeatureEngineeringByDate 的 mr 日期分片,
  .dos 侧为带默认值的尾参(旧调用方行为不变)
- C["ddb_preload_alpha_libs"]=False:D5 惰性加载的逃生开关正式注册

配置注释写明 2 核/8GB 的内存模型:单任务 ≈ days_step × 股票数 ×
基础字段数 × 8B,8GB 建议 days_step ≤ 504。

Co-Authored-By: Hugo <shen.lan123@gmail.com>
- CHANGELOG.md 记录本轮 17 个提交的完整变更(bug/并发/质量/性能)
- ddb_qlib/README.md 补充进程内缓存失效指南(invalidate_ddb_caches)
  与社区版 2 核/8GB 的批次参数调优说明

Co-Authored-By: Hugo <shen.lan123@gmail.com>
Copilot AI review requested due to automatic review settings July 21, 2026 14:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@hugo2046

Copy link
Copy Markdown
Owner Author

Code review

Found 1 issue:

  1. QlibDataLoader._load_lock 收窄到 is_using_dolphindb() 条件后,破坏了引入该锁的原始回归测试 tests/data_mid_layer_tests/test_dataloader_concurrency.py(复现:python -m pytest tests/data_mid_layer_tests/test_dataloader_concurrency.py -q → 80 failed,失败信息即当初 9be4b72 修复的跨线程 instruments 污染)。该测试通过注入不安全共享层的子类模拟竞态、不依赖 database_uri 配置,收窄后锁不再生效。此外 _load_lock 类注释(L162-169)仍宣称“串行化整个 load 体……对本仓库所有消费方安全”,与收窄后的行为矛盾。建议:要么恢复无条件锁,要么同步改写该回归测试与类注释,明确锁的作用域是“仅 DDB 后端”并说明自定义共享态子类需自行加锁。

def load(self, instruments=None, start_time=None, end_time=None) -> pd.DataFrame:
# 跨线程数据污染仅在 DolphinDB 后端出现(单一共享 session + 全局 H 缓存);
# 文件后端本就线程安全,加锁反而是吞吐回归,故锁收窄到 DDB 路径。
# DDB 路径在会话级 session_lock(B1)之外保留本 load 级锁,作为全局 H
# MemCache 写模式的双保险。详见 _load_lock 注释。
from ..data import is_using_dolphindb
if is_using_dolphindb():
with QlibDataLoader._load_lock:
return super().load(instruments, start_time, end_time)
return super().load(instruments, start_time, end_time)

相关:原始修复引入的回归测试 https://github.com/hugo2046/qlib-ddb/blob/3a9cac099f06414ac2691b3ab566c76ab02621c6/tests/data_mid_layer_tests/test_dataloader_concurrency.py 与类注释

# 类级锁:跨所有 QlibDataLoader 实例共享(类变量形式的全局互斥锁)。
# 并发 load() 会共享 qlib 全局数据层(DolphinDB session / 内部缓存),该层非线程
# 安全,并发求值时共享缓冲被互相覆盖——线程 A 请求 instruments X、线程 B 请求
# instruments Y(X∩Y=∅),并发执行后 A 的返回会混入 Y 的 instruments(反之亦然)。
# 串行化整个 load 体是已知最小正确修法(吞吐换正确性,对本仓库所有消费方安全)。
# Reentrancy:load_group_df → D.features 不会回调本类 load / 不再获取本锁,无嵌套
# 加锁风险,故用 Lock 而非 RLock。
_load_lock = threading.Lock()

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

- 取前序期:Python 侧复用 qlib 算子树 get_extended_window_size 解析
  每批表达式的(向前, 向后)外扩交易日数(嵌套递归、双臂取 max、
  Ref($close,-2) 标签向后外扩),经 FeatureEngineeringByDate 尾参
  lookbackDays/rightDays 下发;DDB 端外扩查询窗口计算后把结果矩阵
  截断回请求区间,消除 Mean($close,20) 等在区间头部的窗口不足 NaN。
  qlib 解析不了的表达式退回正则扫窗口 + C["ddb_lookback_default"]
  (默认 252)兜底。
- 分段计算:移除从未生效的 repartitionDS(RANGE,[start,end])+mr
  (2 个边界只产生 1 个数据源的"虚假分区")。改为内存估算放得下
  整段单次计算;放不下按 ddb_days_step 切段顺序循环,每段带
  lookback 重叠(halo)、段内截断,统一列标签对齐后 concatMatrix
  纵向合并,分段边界处滚动结果与整段一致。
- 兼容:FeatureEngineeringByDate 新尾参默认 0,旧调用行为不变;
  D.features() 签名不变。离线单测 +12,DDB 端行为待 live 验证。

Co-Authored-By: Hugo <shen.lan123@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hugo2046

Copy link
Copy Markdown
Owner Author

Code review

针对新增提交 ab525e91(取前序期 + 真实分段计算)复查,发现 2 个问题,均位于本 PR 自述「待 live 验证」的分段/兜底路径:

  1. mergeSegmentResults 在任一分段计算失败时会丢弃该表达式其余分段的正确结果。分段路径下,某个分段对某表达式失败/空结果时 computeRangeFeatures 存入 createEmptyTable(...)(TABLE 而非 MATRIX)占位;合并时一旦命中非 MATRIX 的 part,即 merged[alias] = part; break,把已累积在 mats 里的其它分段矩阵全部丢弃,整个区间的该 alias 塌缩为单个短占位表——与提交自述「分段边界处滚动结果与整段计算一致」相矛盾。仅在内存不足的分段分支触发。建议:跳过失败分段(不 break),或对该 alias 全区间统一回退占位。

continue;
};
part = seg[alias];
if(regexFind(typestr(part), "MATRIX") == -1){
merged[alias] = part;
broken = true;
break;
};
if(part.rows() > 0){
mats.append!(part);

  1. 回看窗口正则兜底会把表达式中的普通数值常量误判为超大回看窗口。get_expression_extended_window 的兜底路径(qlib 解析不了时,即 DDB 专属 alpha 函数)取最大独立整数为窗口:gtjaAlpha191_005($volume/1000000, $close) 会被 _STANDALONE_INT_PATTERN 解析出 1000000,进而 shiftTradeDays(qStart, -1000000) 向前外扩百万交易日,在 2 核/8GB 上触发超量查询(OOM 风险)。结果因末端截断仍正确,属资源/性能问题;现有 12 个测试未覆盖含大常量的兜底表达式。建议:对兜底窗口设上限(如 clamp 到日历范围或一个合理上界)。

except Exception:
ints = [int(s) for s in _STANDALONE_INT_PATTERN.findall(expr)]
lft_etd = max(ints) if ints else int(default_lookback)
futures = [int(s) for s in _FUTURE_INT_PATTERN.findall(expr)]
return lft_etd, max(futures) if futures else 0

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

代码审查(PR #3)发现的两个问题:

- mergeSegmentResults 在某分段计算失败/空时遇占位 TABLE 即 break,
  丢弃该表达式其余分段已算出的正确矩阵。改为失败/空分段统一由
  buildPlaceholderMatrix 产出 NULL 占位矩阵(行-该段交易日,列-
  presentCodes),与正常结果同为带标签矩阵;合并时按行拼接、不再
  丢弃其它分段,顺带消除空 baseData 分段的日期空洞与 Python 侧
  TABLE/MATRIX 类型不一致隐患。移除因此零调用的 createEmptyTable。
- get_expression_extended_window 正则兜底取最大整数为窗口,会把
  数值常量(如 $volume/1000000)误判为百万日回看,触发超量查询。
  新增 _MAX_FALLBACK_WINDOW=2000 上界剔除常量,无候选则退回
  ddb_lookback_default。

离线套件 122 项通过(新增 3 项兜底单测);DDB 端占位矩阵/拼接行为
待 live 验证。

Co-Authored-By: Hugo <shen.lan123@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hugo2046

Copy link
Copy Markdown
Owner Author

审查问题修复(6ee10965)

两个问题均已修复:

1. mergeSegmentResults 丢弃正确分段 — 失败/空分段的占位改由 buildPlaceholderMatrix 统一产出「行-该段交易日、列-presentCodes」的 NULL 矩阵,与正常结果同为带标签矩阵。合并时对所有分段一视同仁按行拼接,任一分段异常不再 break 丢弃其它分段;顺带消除空 baseData 分段的日期空洞与 Python 侧 TABLE/MATRIX 类型隐患。占位填 NULL(非 0)避免污染因子值,并移除因此零调用的 createEmptyTable

def mergeSegmentResults(segResults, colLabels){
merged = dict(STRING, ANY, true);
aliases = [];
for(seg in segResults){
for(k in keys(seg)){
if(!(k in aliases)){
aliases.append!(k);
};
};
};
for(alias in aliases){
mats = [];
rowLabs = array(DATE, 0);
for(seg in segResults){
if(!(alias in keys(seg))){
continue;
};
part = seg[alias];
// 防御:占位统一为矩阵后此处正常不触发;非矩阵则跳过该段,
// 保留其它分段已算出的正确矩阵,不整体丢弃
if(regexFind(typestr(part), "MATRIX") == -1){
continue;
};
if(part.rows() > 0){
mats.append!(part);
rowLabs.append!(part.rowNames());
};
};
if(size(mats) == 0){
continue;
};
if(size(mats) == 1){
merged[alias] = mats[0];
continue;
};
m = concatMatrix(mats, false);
m.rename!(rowLabs, colLabels);
merged[alias] = m;
};
return merged;
};

2. 正则兜底误判数值常量 — 新增 _MAX_FALLBACK_WINDOW=2000(约 8 年交易日)上界,扫到的整数超过它即视为常量剔除,剔除后无候选则退回 ddb_lookback_default$volume/1000000 中的 1000000 不再被当成回看窗口。

# 而非滚动窗口,避免误判成百万日回看导致 shiftTradeDays 外扩到不合理区间。
# 约 8 年交易日,远超常见滚动窗口(年化 252、双年 504 等)
_MAX_FALLBACK_WINDOW: int = 2000
@functools.lru_cache(maxsize=4096)
def get_expression_extended_window(expr: str, default_lookback: int) -> Tuple[int, int]:

离线套件 122 项通过(新增 3 项兜底单测)。DDB 端占位矩阵/拼接行为已列入 live 验证清单(含「单分段失败不丢弃其它分段」用例)。

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants