用 C++ 编写的 DuckDB 扩展,可直接通过 SQL 读取通达信(TDX)股票行情数据文件(.lc1、.lc5、.day)。
- 支持三种通达信文件格式:1 分钟线(
.lc1)、5 分钟线(.lc5)、日线(.day) - 通过文件扩展名自动识别格式,无需手动指定
- 单一 SQL 表函数
read_tdx('文件路径'),接口简洁 - 基于 C_STRUCT ABI,兼容 Python
duckdbpip 包(v1.4.4),无需额外动态库 - 输出标准 OHLCV 列,
datetime列类型为TIMESTAMP
| 列名 | 类型 | 说明 |
|---|---|---|
| datetime | TIMESTAMP | 时间(分钟线含时分,日线为当天 00:00) |
| open | FLOAT | 开盘价 |
| high | FLOAT | 最高价 |
| low | FLOAT | 最低价 |
| close | FLOAT | 收盘价 |
| volume | BIGINT | 成交量(股) |
| amount | DOUBLE | 成交额(元) |
三种格式均为32 字节/记录、小端序、无文件头的二进制文件。
| 偏移 | 大小 | 类型 | 含义 |
|---|---|---|---|
| 0 | 2 | uint16 | 日期:(年-2004)×2048 + 月×100 + 日 |
| 2 | 2 | uint16 | 时间:从午夜起的分钟数(如 571 = 09:31) |
| 4 | 4 | float32 | 开盘价 |
| 8 | 4 | float32 | 最高价 |
| 12 | 4 | float32 | 最低价 |
| 16 | 4 | float32 | 收盘价 |
| 20 | 4 | float32 | 成交额(元) |
| 24 | 4 | uint32 | 成交量(股) |
| 28 | 4 | uint32 | 未知字段(已忽略) |
| 偏移 | 大小 | 类型 | 含义 |
|---|---|---|---|
| 0 | 4 | uint32 | 日期:YYYYMMDD 整数 |
| 4 | 4 | uint32 | 开盘价 × 100(如 338569 = 3385.69) |
| 8 | 4 | uint32 | 最高价 × 100 |
| 12 | 4 | uint32 | 最低价 × 100 |
| 16 | 4 | uint32 | 收盘价 × 100 |
| 20 | 4 | float32 | 成交额(元) |
| 24 | 4 | uint32 | 成交量(股) |
| 28 | 4 | uint32 | 未知字段(已忽略) |
- macOS(Apple Silicon / arm64)
- CMake ≥ 3.15
- Python 3 +
duckdbpip 包 v1.4.4 - DuckDB v1.4.4 开发库(头文件 +
libduckdb.dylib)
mkdir -p /tmp/duckdb_dev_144
cd /tmp/duckdb_dev_144
# 下载 macOS universal 包
curl -L https://github.com/duckdb/duckdb/releases/download/v1.4.4/libduckdb-osx-universal.zip -o libduckdb.zip
unzip libduckdb.zipgit clone https://github.com/donge/tdx_ext.git
cd tdx_ext
./build_and_inject.sh脚本会自动完成:
- CMake 编译扩展(
.so→.duckdb_extension) - 向扩展二进制末尾注入 512 字节 DuckDB 元数据 footer(平台、ABI 类型等)
构建产物:build/tdx.duckdb_extension
cmake -S . -B build
cmake --build build
python3 build_and_inject.sh # 重新注入 footerimport duckdb
db = duckdb.connect(config={"allow_unsigned_extensions": True})
db.execute("LOAD 'build/tdx.duckdb_extension'")
# 读取 1 分钟线
df = db.execute("SELECT * FROM read_tdx('/path/to/sh000001.lc1')").df()
# 读取 5 分钟线
df = db.execute("SELECT * FROM read_tdx('/path/to/sh000001.lc5')").df()
# 读取日线
df = db.execute("SELECT * FROM read_tdx('/path/to/sh000001.day')").df()
# 支持完整 SQL 查询
result = db.execute("""
SELECT
strftime(datetime, '%Y-%m') AS month,
AVG(close) AS avg_close
FROM read_tdx('/path/to/sh000001.day')
WHERE datetime >= '2024-01-01'
GROUP BY month
ORDER BY month
""").fetchall()python3 test_tdx.py测试脚本会验证:
- 扩展正常加载
- 输出列名和类型与预期一致
- 三种格式的记录数与预期相符
- 打印样本行和日期范围
tdx_ext/
├── src/
│ ├── tdx_extension.cpp # 扩展主体(C_STRUCT ABI,表函数实现)
│ ├── tdx_reader.cpp # 文件类型检测
│ └── include/
│ ├── tdx_extension.hpp # 扩展头文件(占位)
│ └── tdx_reader.hpp # TdxFileType 枚举 + DetectTdxFileType 声明
├── CMakeLists.txt # 独立 CMake 构建(指向 /tmp/duckdb_dev_144)
├── build_and_inject.sh # 一键构建 + 注入元数据脚本
└── test_tdx.py # 功能测试脚本
Python duckdb pip 包将 DuckDB 静态编译进 _duckdb.cpython-*.so,不导出任何 DuckDB C++ 符号。因此:
- CPP ABI(依赖 C++ vtable 符号)无法使用
- C_STRUCT ABI:DuckDB 在加载扩展时通过
duckdb_extension_access::get_api()将所有 C API 函数指针打包成duckdb_ext_api_v1结构体传入,扩展通过该结构体调用所有 DuckDB 功能,无需在dlopen时解析任何 DuckDB 符号
入口函数签名:
bool tdx_init_c_api(duckdb_extension_info info, duckdb_extension_access *access);access->get_api()返回的指针仅在tdx_init_c_api()调用期间有效(指向调用栈上的临时对象)。因此在初始化时必须将整个结构体复制到全局变量,而不是仅保存指针。- macOS 构建使用
-undefined dynamic_lookup,运行时符号由宿主进程(Python 解释器内的 DuckDB)提供。 - 扩展二进制需在末尾附加 512 字节元数据 footer(8 个字段 × 32 字节,逆序存储 + 256 字节签名),并以
allow_unsigned_extensions: True加载。
MIT