当前位置:首页>python>python解析迅投qmt本地dat文件格式

python解析迅投qmt本地dat文件格式

  • 2026-10-16 09:08:37
python解析迅投qmt本地dat文件格式

这2天有同学问我:我主要靠 MiniQMT 下载数据, miniqmt客户端一关、我的数据咋办?

这是个非常实在的问题。MiniQMT 的行情下载依赖客户端进程活着:之前可以通过xtquant 的 download_history_data 下载。

最容易想到的办法,用大 QMT(完整版客户端)的"数据管理"功能,把行情批量导出成 CSV 存起来。 或者用qmt代码实现

这里我换个思路:QMT 下载的数据,本来就躺在我电脑硬盘上。它存在哪、长什么样,搞清楚这两个问题,不就直接读了吗?

一、datadir 目录探秘

QMT 客户端把下载的行情放在安装目录(MiniQMT 是 userdata_mini/datadir,大 QMT 类似)下,结构大概是这样:

datadir/

├── SH/                  ← 沪市

│   ├── 60/              ← 周期目录:60 秒 = 1 分钟 K 线

│   │   └── 000001.DAT

│   ├── 86400/           ← 86400 秒 = 日 K 线

│   │   └── 600519.DAT

│   └── 0/               ← tick 分笔数据,按 代码/日期 分目录

│       └── 000001/20250613.dat

└── SZ/                  ← 深市,结构相同

规律很清晰:一级是市场(SH/SZ/BJ),二级是周期(秒数),三级是代码.DAT。tick 数据特殊一些,是 市场/0/代码/日期.dat 的形式,一天一个文件。

二、二进制格式拆解

拿 SH/60/000001.DAT(上证指数分钟线)开刀。先看文件大小:15432 字节。再看内容,用十六进制编辑器打开,很快能发现规律——每 64 字节出现一次形似时间戳的数字,且恰好相差 60。这不是巧合,这就是记录边界:每根 K 线一条记录,固定 64 字节,没有文件头,整个文件就是记录的顺序堆放。

偏移
类型
字段
说明
0–7
—
未解析
跳过
8–11
u32
时间戳
Unix 秒,北京时间
12–15
u32
open
÷1000
16–19
u32
high
÷1000
20–23
u32
low
÷1000
24–27
u32
close
÷1000
28–31
—
未解析
跳过
32–35
u32
volume
成交量,原值
36–39
—
未解析
跳过
40–47
u64
amount
成交额,原值
48–51
u32
open_interest
持仓量,股票为 0
52–59
—
未解析
跳过
60–63
u32
pre_close
昨收,÷1000

三、完整 Python 源码

这里写一个解析分钟级别的解析代码,仅供参考

# -*- coding: utf-8 -*-"""解析 MiniQMT 本地 1 分钟 K 线 .dat 文件文件格式(逆向结论):  - 无文件头,整个文件由若干条固定 64 字节的记录组成(小端)  - 偏移 0-7    : 未解析(跳过)  - 偏移 8-11   : u32 Unix 秒时间戳(北京时间)  - 偏移 12-15  : u32 open  (÷1000)  - 偏移 16-19  : u32 high  (÷1000)  - 偏移 20-23  : u32 low   (÷1000)  - 偏移 24-27  : u32 close (÷1000)  - 偏移 28-31  : 未解析(跳过)  - 偏移 32-35  : u32 volume 成交量  - 偏移 36-39  : 未解析(跳过)  - 偏移 40-47  : u64 amount 成交额(原值)  - 偏移 48-51  : u32 open_interest 持仓量  - 偏移 52-59  : 未解析(跳过)  - 偏移 60-63  : u32 pre_close 昨收(÷1000)用法:  python qmt_min_parser.py <datadir>/SH/60/000001.DAT"""import csvimport osimport structimport sysfrom dataclasses import dataclassfrom datetime import datetime, timezone, timedeltaRECORD_SIZE = 64PRICE_SCALE = 1000.0# 64 字节 = 16 个小端 u32;u64 amount 由相邻两个 u32 拼出RECORD = struct.Struct("<16I")BEIJING = timezone(timedelta(hours=8))@dataclassclass MinKline:    time: datetime      # 北京时间    open: float    high: float    low: float    close: float    volume: int         # 成交量    amount: float       # 成交额(元)    open_interest: int  # 持仓量(股票为 0)    pre_close: float    # 昨收def parse_metadata_from_path(path: str) -> tuple:    """从路径 .../SH/60/000001.DAT 提取 (market, period_sec, symbol)。"""    parts = [p for p in path.replace("\\", "/").split("/") if p]    fname = parts[-1]    symbol = os.path.splitext(fname)[0]    period = parts[-2] if len(parts) >= 2 else ""    market = ""    for p in reversed(parts[:-2]):        if p.upper() in ("SH", "SZ", "BJ"):            market = p.upper()            break    return market, period, symboldef parse_min_file(path: str) -> list:    market, period, symbol = parse_metadata_from_path(path)    bars = []    with open(path, "rb") as f:        data = f.read()    # 不足 64 字节的尾部残片直接丢弃    n = len(data) // RECORD_SIZE    for i in range(n):        u = RECORD.unpack_from(data, i * RECORD_SIZE)        ts      = u[2]                     # 偏移 8        o, h, l, c = u[3], u[4], u[5], u[6]  # 偏移 12/16/20/24        volume  = u[8]                     # 偏移 32        amount  = u[10] | (u[11] << 32)    # 偏移 40,u64        oi      = u[12]                    # 偏移 48        pre_c   = u[15]                    # 偏移 60        bars.append(MinKline(            time=datetime.fromtimestamp(ts, BEIJING),            open=o / PRICE_SCALE,            high=h / PRICE_SCALE,            low=l / PRICE_SCALE,            close=c / PRICE_SCALE,            volume=volume,            amount=float(amount),            open_interest=oi,            pre_close=pre_c / PRICE_SCALE,        ))    return market, period, symbol, barsdef main():    path = sys.argv[1] if len(sys.argv) > 1 else \        r"D:\lwj\国金证券QMT交易端\datadir\SH\60\000001.DAT"    market, period, symbol, bars = parse_min_file(path)    print(f"文件: {path}")    print(f"标的: {market}{symbol}  周期: {period}秒  记录数: {len(bars)}")    if not bars:        print("没有解析到任何记录")        return    days = sorted({b.time.strftime("%Y-%m-%d") for b in bars})    print(f"日期范围: {days[0]} ~ {days[-1]}  覆盖 {len(days)} 个交易日")    print("\n前 5 条:")    print(f"{'时间':<20}{'开':>10}{'高':>10}{'低':>10}{'收':>10}"          f"{'量':>14}{'额':>18}{'昨收':>10}")    for b in bars[:5]:        print(f"{b.time.strftime('%Y-%m-%d %H:%M'):<20}{b.open:>10.3f}"              f"{b.high:>10.3f}{b.low:>10.3f}{b.close:>10.3f}"              f"{b.volume:>14,}{b.amount:>18,.0f}{b.pre_close:>10.3f}")    print("\n后 5 条:")    for b in bars[-5:]:        print(f"{b.time.strftime('%Y-%m-%d %H:%M'):<20}{b.open:>10.3f}"              f"{b.high:>10.3f}{b.low:>10.3f}{b.close:>10.3f}"              f"{b.volume:>14,}{b.amount:>18,.0f}{b.pre_close:>10.3f}")    # 导出 CSV    out = os.path.splitext(os.path.basename(path))[0] + f"_{market}_{period}s.csv"    with open(out, "w", newline="", encoding="utf-8-sig") as f:        w = csv.writer(f)        w.writerow(["time", "open", "high", "low", "close",                    "volume", "amount", "open_interest", "pre_close"])        for b in bars:            w.writerow([b.time.strftime("%Y-%m-%d %H:%M:%S"), b.open, b.high,                        b.low, b.close, b.volume, b.amount, b.open_interest,                        b.pre_close])    print(f"\n已导出 CSV: {os.path.abspath(out)}")if __name__ == "__main__":    main()

四、运行结果

如果你有迅投qmt开户需求,可以找我沟通交流,费率优惠。

最新文章

随机文章