Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,19 @@ permissions:
jobs:
build:

runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11"]
# Windows 是 tdx2db 用户主场景(本地读通达信文件),必须纳入矩阵
os: [ubuntu-latest, windows-latest]
python-version: ["3.9", "3.11"]
include:
- os: ubuntu-latest
python-version: "3.10"
defaults:
run:
shell: bash

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ tdx2db status

`python main.py <子命令>` 与 `tdx2db <子命令>` 等价(main.py 是薄封装,保留老用户习惯)。包目录为 `tdx2db/`(v0.3.0 起从 `src/` 改名,发布到 PyPI)。psycopg2-binary/pymysql 为可选依赖(extras: postgres/mysql/all),默认安装仅支持 SQLite。

测试:`pytest tests/`(processor 纯函数 + 模块导入冒烟,CI 三矩阵 3.9/3.10/3.11 运行)。数据正确性验证仍以运行 `sync` 后检查数据库为准。
测试:`pytest tests/`。CI 矩阵:ubuntu + windows × 3.9/3.11(+ ubuntu 3.10)——Windows 是用户主场景必须覆盖。`tests/fixtures/` 存 .day/.lc5 二进制解析 fixture(合成字节,生成器 `make_fixtures.py`;断言值与生成参数一一对应)。数据正确性验证仍以运行 `sync` 后检查数据库为准。

## 发布 PyPI(维护者)

Expand Down
51 changes: 51 additions & 0 deletions tests/fixtures/make_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""生成 tests/fixtures/ 下的二进制解析 fixture(issue #34)。

fixture 是确定性合成字节,格式与通达信实际文件逐字段一致:
- .day 32 字节/条:<IIIIIfII date, open, high, low, close(价×100 整型), amount(f32), volume, 保留
- .lc5 32 字节/条:<HHfffffII date((年-2004)*2048+月*100+日), time(0点起分钟数),
open/high/low/close/amount(f32), volume(u32), 保留

价格/成交额选用二进制可精确表示的值(×.25/.5/.75),断言可用 ==。
重新生成:python tests/fixtures/make_fixtures.py
真实文件切片(数据盘可用时):dd if=<vipdoc>/sh/lday/sh688001.day of=sh688001.day bs=32 count=5
"""
import struct
from pathlib import Path

HERE = Path(__file__).parent

# 与 tests/test_reader.py 的断言一一对应,改动需同步
DAY_RECORDS_SZ = [
# (date, open, high, low, close, amount, volume)——open 等为 ×100 整型
(20260605, 1050, 1075, 1025, 1050, 5250000.0, 500000),
(20260608, 1050, 1100, 1050, 1075, 5375000.0, 400000),
]
DAY_RECORDS_688 = [
(20260605, 5000, 5150, 4950, 5075, 25375000.0, 300000),
]
LC5_RECORDS = [
# (year, month, day, hour, minute, open, high, low, close, amount, volume)
(2026, 6, 5, 9, 35, 10.5, 10.75, 10.25, 10.5, 2100000.0, 200000),
(2026, 6, 5, 9, 40, 10.5, 10.5, 10.25, 10.25, 1025000.0, 100000),
]


def write_day(path: Path, records) -> None:
with open(path, 'wb') as f:
for date, o, h, l, c, amount, vol in records:
f.write(struct.pack('<IIIIIfII', date, o, h, l, c, amount, vol, 0))


def write_lc5(path: Path, records) -> None:
with open(path, 'wb') as f:
for year, month, day, hour, minute, o, h, l, c, amount, vol in records:
date_num = (year - 2004) * 2048 + month * 100 + day
time_num = hour * 60 + minute
f.write(struct.pack('<HHfffffII', date_num, time_num, o, h, l, c, amount, vol, 0))


if __name__ == '__main__':
write_day(HERE / 'sz000001.day', DAY_RECORDS_SZ)
write_day(HERE / 'sh688001.day', DAY_RECORDS_688)
write_lc5(HERE / 'sz000001.lc5', LC5_RECORDS)
print(f'fixtures written to {HERE}')
Binary file added tests/fixtures/sh688001.day
Binary file not shown.
Binary file added tests/fixtures/sz000001.day
Binary file not shown.
Binary file added tests/fixtures/sz000001.lc5
Binary file not shown.
84 changes: 84 additions & 0 deletions tests/test_reader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""reader 二进制解析回归测试(issue #34)。

fixture 为确定性合成字节(见 tests/fixtures/make_fixtures.py),
断言值与生成参数一一对应。覆盖三条解析路径:
- 深市 .day → pytdx TdxDailyBarReader(A 股系数:价 ×0.01、量 ×0.01)
- 沪市 688 .day → 自研 _read_day_file_raw 回退(pytdx 不识别科创板)
- .lc5 → pytdx TdxLCMinBarReader
"""
import shutil
from pathlib import Path

import pytest

from tdx2db.reader import TdxDataReader

FIXTURES = Path(__file__).parent / 'fixtures'


@pytest.fixture
def tdx_reader(tmp_path):
"""用 fixture 文件搭出最小 vipdoc 目录结构"""
for market, sub, fname in [
('sz', 'lday', 'sz000001.day'),
('sh', 'lday', 'sh688001.day'),
('sz', 'fzline', 'sz000001.lc5'),
]:
dst = tmp_path / 'vipdoc' / market / sub
dst.mkdir(parents=True, exist_ok=True)
shutil.copy(FIXTURES / fname, dst / fname)
return TdxDataReader(tdx_path=str(tmp_path))


class TestDailyParsing:
def test_sz_a_stock_via_pytdx(self, tdx_reader):
df = tdx_reader.read_daily_data(0, 'sz000001')

assert len(df) == 2
assert list(df.index.strftime('%Y-%m-%d')) == ['2026-06-05', '2026-06-08']
first = df.iloc[0]
assert first['open'] == 10.50
assert first['high'] == 10.75
assert first['low'] == 10.25
assert first['close'] == 10.50
assert first['amount'] == 5250000.0
assert first['volume'] == 5000.0 # 500000 × 0.01(pytdx A 股系数)
assert first['code'] == '000001' # 6 位纯数字,无前缀
assert first['market'] == 0

def test_688_via_raw_fallback(self, tdx_reader):
"""688 走 _read_day_file_raw,系数必须与 pytdx SH_A_STOCK 一致"""
df = tdx_reader.read_daily_data(1, 'sh688001')

assert len(df) == 1
row = df.iloc[0]
assert row['open'] == 50.00
assert row['high'] == 51.50
assert row['low'] == 49.50
assert row['close'] == 50.75
assert row['amount'] == 25375000.0
assert row['volume'] == 3000.0 # 300000 × 0.01,与 pytdx 路径同系数
assert row['code'] == '688001'

def test_missing_file_raises(self, tdx_reader):
with pytest.raises(FileNotFoundError):
tdx_reader.read_daily_data(0, 'sz999999')


class TestLc5Parsing:
def test_fivemin_records(self, tdx_reader):
df = tdx_reader.read_5min_data(0, 'sz000001')

assert len(df) == 2
assert list(df['datetime'].dt.strftime('%Y-%m-%d %H:%M')) == [
'2026-06-05 09:35', '2026-06-05 09:40',
]
first = df.iloc[0]
assert first['open'] == 10.5
assert first['high'] == 10.75
assert first['low'] == 10.25
assert first['close'] == 10.5
assert first['amount'] == 2100000.0
assert first['volume'] == 200000 # lc5 无系数换算
assert first['code'] == '000001'
assert first['market'] == 0
Loading