alitrack

200 行 Lua,DuckDB 就能读通达信行情了

网上有篇文章,叫「用 DuckDB 读通达信数据文件」。作者写了个 C 扩展,实现了 read_tdx() 函数,让 .lc1、.lc5、.day 这些通达信行情文件可以直接用 SQL 查。

项目很实用,但有个门槛——C 扩展得编译,得处理 DuckDB 的 C_STRUCT ABI,踩坑不少。作者自己也说:「ABI 不对就崩」「存指针不如复制结构体」。

我就在想,如果把这个功能用 Lua 写,会是什么样?

● ● ●

200 行

两个小时后,结果出来了。

libs/datasource/tdx.lua,200 行。零编译,零依赖,luajit_module(mode := 'install', sql_name := 'tdx') 一条 SQL 装上就能用。

对比一下 C 版本:

  • C 扩展:tdx_extension.cpp + tdx_reader.cpp + tdx_reader.hpp,三个文件,~400 行,需要 CMake + vcpkg 编译链
  • Lua 版本:一个文件,200 行,纯 Lua + LuaJIT FFI(只有 float32 转字节用到了 FFI)

● ● ●

通达信文件格式

先说格式。通达信的行情文件很简单,三种格式,都是 32 字节一条记录,小端序,没有文件头。

日线(.day),32 字节:

偏移
大小
内容
0
4
日期(YYYYMMDD)
4
4
开盘价 × 100
8
4
最高价 × 100
12
4
最低价 × 100
16
4
收盘价 × 100
20
4
成交额(float32)
24
4
成交量
28
4
未知

分钟线(.lc1 / .lc5),也是 32 字节:

偏移
大小
内容
0
2
日期((年-2004)×2048 + 月×100 + 日)
2
2
时间(午夜起分钟数)
4
4
开盘价(float32)
8
4
最高价(float32)
12
4
最低价(float32)
16
4
收盘价(float32)
20
4
成交额(float32)
24
4
成交量
28
4
未知

● ● ●

最麻烦的部分

写解析器本身不复杂——string.byte() 读字节,套公式算日期,LuaJIT FFI 把 4 个字节转成 float32。

最麻烦的是日期解码。通达信的日期编码很"土":

  • 日线:YYYYMMDD 整数,直接除 10000 取整
  • 分钟线:(年-2004)×2048 + 月×100 + 日,时间从午夜算分钟数

都不难,但第一次写的人得对着 hexdump 猜半天。C 扩展的作者也说了:「格式全靠猜,32 字节一条记录,小端序,没有文件头,只能 hexdump 对着软件显示的价格一个字段一个字段猜。」

● ● ●

怎么用

装好 duckdb-luajit 扩展后,一条 SQL 装上:

SELECT * FROM luajit_module(mode := 'install', sql_name := 'tdx');

然后查数据:

SELECT split_part(val,'|',1) AS datetime,
       split_part(val,'|',2)::FLOAT AS open,
       split_part(val,'|',3)::FLOAT AS high,
       split_part(val,'|',4)::FLOAT AS low,
       split_part(val,'|',5)::FLOAT AS close,
       split_part(val,'|',6)::BIGINT AS volume,
       split_part(val,'|',7)::DOUBLE AS amount
FROM luajit_table('tdx', list := '/path/sh000001.day');

输出格式是 pipe 分隔的字符串,datetime|open|high|low|close|volume|amount。分钟线的时间精确到秒,日线只到天。

查最高收盘价:

SELECT MAX(split_part(val,'|',5)::FLOAT) AS max_close
FROM luajit_table('tdx', list := '/path/sh000001.day');

● ● ●

为什么用 Lua 写

DuckDB 的扩展生态目前分成两派:

C 扩展性能好,但要编译、要处理 ABI 兼容、要打包成 .duckdb_extension 二进制。Python duckdb 的 pip 包是静态编译的,不导出任何 C++ 符号,加载 C 扩展就得用 C_STRUCT ABI,文档几乎没有,全靠翻源码。

Lua 扩展(duckdb-luajit)不用编译,一条 SQL 从仓库拉下来就能用。200 行的解析器,如果用 C 写,光 CMakeLists.txt 和 vcpkg.json 就占了一半行数。

不是说 C 不好——性能场景当然用 C。但通达信文件解析是 I/O 密集型,瓶颈在磁盘,CPU 那点差异可以忽略。用 Lua 省掉的编译时间、部署成本、ABI 调试,比那点性能差划算得多。

● ● ●

代码

代码在 duckdb-luajit-libs 仓库的 libs/datasource/tdx.lua:

github.com/alitrack/duckdb-luajit-libs

MIT 协议,随便用。