
簡介面向量化投資與金融數據分析的Python實用腳本解決從通達信TDX本地數據文件中批量提取股票日線數據的難題適合需要對接通達信行情數據、開展數據分析和策略回測的Python開發(fā)者。壓縮包內共1個文件為ReadFile.py腳本整體大小僅2KB腳本雖短小但完整涵蓋安裝路徑定位、數據文件解析、字段清洗以及轉為pandas DataFrame等關鍵流程可適配用戶自定義的通達信安裝目錄。腳本使用os、pandas、struct等模塊演示解析日期、開盤價、收盤價、最高價、最低價和成交量等核心字段并輸出結構化的數據供后續(xù)分析。目前已有2192人學習瀏覽。通過研讀此腳本讀者能掌握讀取非標準金融數據文件的常用思路直接獲得可運行的日線數據提取工具為后續(xù)行情分析、量化建模和投資決策提供高效、可復用的數據基礎。1. 通達信日線數據其實是個二進制文件readFile_tdx 干的就是解析它寫回測腳本最卡殼的一步不是策略而是行情從哪來。拉公開接口要鑒權、要限頻盤中可能被斷流下班回家網絡一波動整個研究環(huán)境就癱了。通達信客戶端只要日常打開過收盤后就會把全市場日線寫進本地vipdoc/**/lday/*.day文件里這些文件沒有索引、沒有表頭每條 K 線固定占用 32 字節(jié)。標題里的ReadFile.rar是這類解析工具在論壇里最常見的打包名里面的 readFile_tdx 本質就是把 32 字節(jié)二進制塊掰成日期、開高低收、成交額、成交量。這套姿勢和 tdx 服務端接口無關純本地、極快適合離線回測和數據歸檔。本文直接用 Python 標準庫加 pandas 把它寫出來順帶把批量入庫、增量更新和文件校驗一起收尾。2. 通達信 .day 日線格式32 字節(jié)一條記錄字段順序別搞反2.1 vipdoc 目錄下的文件名怎么對應市場與代碼通達信安裝目錄下能找到vipdoc文件夾里面按交易所分了sh、sz等子目錄日線文件統(tǒng)一放在lday子目錄下文件名格式是“市場前綴 6 位證券代碼 .day”。例如滬市貴州茅臺的日線文件是sh600519.day深市平安銀行是sz000001.day。想讀取哪只股票先在本地把這個路徑拼出來再交給解析函數。這個文件名規(guī)則同時也是數據完整性的第一道防線程序里習慣把fp.stem直接當作證券代碼字段寫入 DataFrame這樣后面按股票分組、做增量更新都不需要額外維護映射表。部分通達?版本還支持北交所等新市場目錄前綴可能不同但結構一致交易所目錄/lday/前綴代碼.day。我一般掃描時直接glob(*/lday/*.day)不寫死市場列表新增市場也能自動覆蓋。2.2 32 字節(jié)記錄的字節(jié)級字段表.day文件是典型的二進制定長記錄文件每條 K 線固定 32 字節(jié)字節(jié)序為小端x86 架構默認。字段順序按照通達信約定固定不變錯一個字節(jié)后面的數據全部錯位。官方沒有公開過格式文檔這是多年逆向和經驗積累出的通用布局偏移量字節(jié)數類型字段名存儲說明04uint32日期形如 20240105無分隔符44uint32開盤價實際價格放大 100 倍后取整存放84uint32最高價同上124uint32最低價同上164uint32收盤價同上204float32成交額單位元244uint32成交量單位股284uint32保留字段通常為 0不參與計算價格字段用整數存放大值這一設計很關鍵A 股價格精確到分把 7.91 存成 791比存浮點更穩(wěn)定也避免了 float32 在反復計算時累積誤差。解析后必須手動除以 100 還原。IIIIIfII是標準的 struct 解包格式串其中表示小端I是無符號 32 位整數f是單精度浮點。成交額列用的是 float32單精度只有約 7 位有效數字遇到單日成交額幾百億的極端情況會有幾十元誤差做資金流分析時要留意普通量價回測不受影響。2.3 打開文件先做 32 字節(jié)對齊校驗復制過的 .day 文件偶爾會出現(xiàn)尾部殘缺原因可能是硬盤壞道、U 盤未安全彈出或通達信寫盤時被強制關閉。讀取之前先做一次字節(jié)數對齊校驗能避免解析到一半莫名報錯。from pathlib import Path for fp in Path(vipdoc/sh/lday).glob(*.day): size fp.stat().st_size if size % 32 ! 0: print(f{fp.name}: {size} 字節(jié)不是 32 的倍數尾部有殘缺) elif size 0: print(f{fp.name}: 空文件)這段腳本只做兩件事先通過st_size取文件字節(jié)數再判斷能否被 32 整除。文件大小正好是 32 的整數倍說明每條 K 線記錄都完整有余數意味著最后一條記錄被截斷。glob(*.day)只匹配當前目錄不遞歸如果要掃描滬深兩市把路徑寫成Path(vipdoc).glob(*/lday/*.day)即可。校驗通過后才能放心交給后續(xù)的解析函數。3. 用 Python 寫 readFile_tdx一個函數讀完全部日線論壇里流轉的ReadFile.rar解壓出來腳本命名出現(xiàn)過readFile_tdx.py、readFile_tentmev.py幾個版本名字有出入做的事情完全一樣。我一般不會直接用壓縮包里的代碼因為里面經常混著過時的 print 調試和硬編碼路徑而是按下面這個最小實現(xiàn)自己維護。3.1 struct.unpack 逐個解析 32 字節(jié)所有字段一次解出import struct from pathlib import Path import pandas as pd TDX_DAY_STRUCT IIIIIfII def readFile_tdx(file_path: str | Path) - pd.DataFrame: 讀取通達信日線 .day 文件返回 DataFrame fp Path(file_path) if not fp.exists() or fp.stat().st_size 0: return pd.DataFrame(columns[date, open, high, low, close, amount, volume]) rows [] with fp.open(rb) as f: while True: block f.read(32) if len(block) 32: break date_int, open_px, high_px, low_px, close_px, amount, volume, reserved \ struct.unpack(TDX_DAY_STRUCT, block) rows.append((date_int, open_px / 100.0, high_px / 100.0, low_px / 100.0, close_px / 100.0, amount, volume)) df pd.DataFrame(rows, columns[date, open, high, low, close, amount, volume]) df[date] pd.to_datetime(df[date].astype(str), format%Y%m%d) return df.sort_values(date).reset_index(dropTrue)f.read(32)每次讀一條固定長度記錄len(block) 32時直接 break等于把 2.3 里的尾部殘缺校驗內聚到了讀取函數里碰到不完整塊就靜默丟棄不拋異常中斷整個批次。struct.unpack返回 8 個值一一對應字段表。價格字段全部除以 100.0這里用浮點除法而不是整除避免價差被截斷。日期列先用astype(str)把整數20240105變成字符串再交給pd.to_datetimeformat%Y%m%d是為了明確告訴 pandas 日期的排列方式不加會走默認推斷慢且容易出警告。3.2 numpy.fromfile 向量化版本讀取速度更快struct 循環(huán)對每根 K 線做一次 unpackPython 函數調用開銷疊加后讀全市場五千只股票會明顯發(fā)慢。換 numpy 的fromfile可以直接把整個文件映射成結構化數組C 層面完成解析速度提升一個數量級。import numpy as np TDX_DAY_DTYPE np.dtype([ (date, u4), (open, u4), (high, u4), (low, u4), (close, u4), (amount, f4), (volume, u4), (reserved, u4), ]) def readFile_tdx_np(file_path: str | Path) - pd.DataFrame: fp Path(file_path) if not fp.exists() or fp.stat().st_size 0: return pd.DataFrame(columns[date, open, high, low, close, amount, volume]) n fp.stat().st_size // TDX_DAY_DTYPE.itemsize arr np.fromfile(fp, dtypeTDX_DAY_DTYPE, countn) df pd.DataFrame({ date: pd.to_datetime(arr[date].astype(str), format%Y%m%d), open: arr[open] / 100.0, high: arr[high] / 100.0, low: arr[low] / 100.0, close: arr[close] / 100.0, amount: arr[amount], volume: arr[volume], }) return df.sort_values(date).reset_index(dropTrue)np.dtype里的字段名和類型必須和 struct 版本嚴格一致u4表示小端無符號 4 字節(jié)整數f4表示小端單精度浮點。itemsize自動計算為 32st_size // itemsize算出完整記錄條數count參數保證即使文件尾部有殘缺numpy 也只會讀取完整記錄不會拋出字節(jié)數不匹配的錯誤。這個細節(jié)容易踩坑不傳count時fromfile遇到不完整的結構化記錄可能直接拋異常傳了就等于把 2.3 的校驗下沉到了讀取層。3.3 價格還原、日期處理和異常 K 線過濾結構化數組直接取列再除 100比 struct 版本代碼更短但有個隱含差異numpy 的u4除法會產生浮點數組amount字段讀出來是 float32直接丟進 DataFrame 后精度保持不了那么多位。若對成交額精度敏感可以在 DataFrame 構造后round(2)否則只影響展示不影響回測計算。數據進 DataFrame 后還缺兩道清洗。第一通達信部分版本寫盤順序并非嚴格升序尤其是手工復制過的文件因此最后統(tǒng)一sort_values(date)并把索引重置。第二市場偶爾出現(xiàn)停牌日連續(xù)幾天沒有成交成交量字段為 0 是合法的但收盤價為 0 的臟數據需要剔除df df[df[close] 0] df df.drop_duplicates(subset[date])close 0過濾掉未初始化的空記錄drop_duplicates以日期為鍵去重防止通達信兩次寫盤導致同一根 K 線出現(xiàn)兩條。過濾邏輯要放在日期轉換之后因為null日期參與排序會干擾結果。4. 批量讀取全部 A 股日線落成 Parquet 并做增量更新4.1 用 pathlib 遍歷 vipdoc一次讀出全市場日線單只股票的讀取函數能跑通后下一步是把整個vipdoc目錄變成一個大 DataFrame。常見做法是按“市場目錄/lday”的層級做一次遞歸遍歷把所有.day文件路徑收集起來逐個調用讀取函數再給每個 DataFrame 打上證券代碼標簽。def build_daily_library(vipdoc_root: str | Path) - pd.DataFrame: root Path(vipdoc_root) files sorted(root.glob(*/lday/*.day)) print(f發(fā)現(xiàn) {len(files)} 個日線文件) frames [] for fp in files: df readFile_tdx_np(fp) if df.empty: continue df[code] fp.stem frames.append(df) return pd.concat(frames, ignore_indexTrue)glob(*/lday/*.day)兩層通配能同時覆蓋sh和sz目錄fp.stem取出文件名去掉后綴的部分正好是sh600519這樣的完整證券標識。pd.concat時設置ignore_indexTrue重建全局索引避免每只股票都從 0 開始導致后面分組操作混亂。這個函數的瓶頸不在解析而在 DataFrame 構造五千個文件逐個構造小 DataFrame 再合并總體耗時幾十秒量級跑批可以接受。4.2 CSV、Parquet 還是 SQLite存儲取舍全市場日線常年累計下來會有幾百萬行每次重讀.day文件雖然快但重復解析同一份數據不值得。讀取一遍后把它緩存成中間格式后續(xù)研究直接加載緩存。三種常見存儲方案各有定位存儲格式讀取速度壓縮率適用場景CSV慢低臨時交換、肉眼檢查Parquet快高分析主存儲列式讀取SQLite中等中等頻繁按 code 點查與單條更新我用得最多的是 Parquet按列存儲回測時只加載date、close兩列比全量加載省很多 IO。寫入時指定pyarrow引擎和zstd壓縮壓縮率通常能到原始 CSV 的五分之一以上df.to_parquet(daily_lib.parquet, enginepyarrow, compressionzstd, indexFalse)Parquet 的劣勢是不支持原地更新增量合并時要把新老數據讀出來再整體覆寫。如果不需要列存加速SQLite 按code date建唯一索引后做 upsert 更方便適合小體量的個人行情庫。CSV 除非要發(fā)給別人看否則不建議作為主存儲幾百萬行 CSV 光解析就要花幾十秒。4.3 增量更新最后 K 線日期到今天的間隔決定要不要重讀每次收盤后全量重建行情庫有點浪費更合理的做法是只追加缺失日期。判斷標準很簡單拿現(xiàn)有庫中每只股票的最后 K 線日期和當天日期算間隔超過閾值就觸發(fā)重讀。from datetime import timedelta last_dates df.groupby(code)[date].max() today pd.Timestamp.today().normalize() stale last_dates[last_dates today - pd.Timedelta(days5)] print(f有 {len(stale)} 只股票超過 5 個自然日未更新)這里面有個容易誤解的點文件日期和刷新時間不是一回事。通達信收盤后不一定立即把所有股票寫盤某些冷門股可能延遲到次日才補數據所以 5 個自然日的閾值比“今天必須更新”更穩(wěn)妥。groupby(code)[date].max()是按證券代碼取每個分組最大日期normalize()把當前時間歸零到當天零點避免小時分鐘干擾日期差計算。拿到過期名單后再去對應.day文件增量解析最后合并寫回 cache。這個思路等同于搜索里常問的“通達信指定日期到今天的天數”只不過日期不是手動輸入而是從最后一條 K 線里取。5. 文件完整性與除權缺口兩個驗證技巧5.1 用文件大小與 K 線數量做一致性校驗數據入庫后要先證明沒問題再拿去做回測。最簡單的一致性校驗是交叉驗證文件大小除以 32 等于理論 K 線條數解析結果的行數必須和它一致。任何對不上都說明解析有遺漏或文件已經損壞。for fp in sorted(Path(vipdoc/sh/lday).glob(*.day)): size fp.stat().st_size expected size // 32 df readFile_tdx_np(fp) if size % 32 ! 0: print(f{fp.name}: 文件字節(jié)數異常) elif len(df) ! expected: print(f{fp.name}: 解析 {len(df)} 條期望 {expected} 條)expected是理論完整記錄條數len(df)是實際解析條數。正常情況下兩者相等不等時優(yōu)先懷疑 3.3 的過濾條件誤傷了合法記錄比如close為 0 的歷史原始數據。運行完整個目錄后批處理日志里出現(xiàn)過的文件名應該為零這就是全庫健康的靜態(tài)證據。這個腳本耗時很短適合每次跑增量前墊一道。5.2 用相鄰 K 線跳空定位除權日維護復權因子表.day文件本身是不復權數據除權除息日當天價格會垂直跳空前收盤價和后開盤價之間的缺口會被回測誤認為巨大跌幅。識別這類缺口有一個廉價方法計算每根 K 線開盤價相對前一根收盤價的變化率超過正常漲跌停幅度如 9.8%的位置就標記為疑似除權日。prev_close df[close].shift(1) gap df[open] / prev_close - 1 suspects df.index[(gap.abs() 0.098) (prev_close 0)]shift(1)把收盤價整體下移一行形成“前一根收盤價”序列開盤價除以它減 1 得到跳空幅度。prev_close 0排除停牌后復牌導致的空值。漲停、跌停也會觸發(fā)大于 9.8% 的閾值所以這張表只是嫌疑名單要和通達信頁面的除權信息復核。確認是除權日后把因子記入獨立的復權因子表后續(xù)計算前復權價時用累計因子乘回去。記住一個原則原始.day永遠保留不復權版本復權結果單獨生成這樣因子表出錯了還能一鍵重算。把復權因子表單獨存檔增量更新時用同一組規(guī)則重算一遍即可。本文還有配套的精品資源點擊獲取