From 73c9cce6e98ec9096c7c84b64f62a4e861c6f404 Mon Sep 17 00:00:00 2001 From: xbfighting Date: Tue, 7 Jul 2026 15:39:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Windows=20CI=20=E7=9F=A9=E9=98=B5=20+?= =?UTF-8?q?=20.day/.lc5=20=E4=BA=8C=E8=BF=9B=E5=88=B6=E8=A7=A3=E6=9E=90=20?= =?UTF-8?q?fixture=20=E6=B5=8B=E8=AF=95=20(#34)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CI 矩阵扩展为 ubuntu+windows × 3.9/3.11(+ ubuntu 3.10), Windows 是本地读通达信文件的用户主场景;run 步骤统一 bash shell - tests/fixtures/:确定性合成字节 fixture(.day 深市 / 688 / .lc5), 生成器 make_fixtures.py 附真实文件切片方法(dd)备后续替换 - tests/test_reader.py:三条解析路径逐字段断言(pytdx A 股系数路径 / 688 自研 struct 回退路径 / lc5 分钟线),共 4 个测试 - 对标调研吸收(easy_tdx 的 fixture 回归法与 Windows 矩阵实践) Co-Authored-By: Claude Fable 5 --- .github/workflows/python-package.yml | 12 +++- CLAUDE.md | 2 +- tests/fixtures/make_fixtures.py | 51 ++++++++++++++++ tests/fixtures/sh688001.day | Bin 0 -> 32 bytes tests/fixtures/sz000001.day | Bin 0 -> 64 bytes tests/fixtures/sz000001.lc5 | Bin 0 -> 64 bytes tests/test_reader.py | 84 +++++++++++++++++++++++++++ 7 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/make_fixtures.py create mode 100644 tests/fixtures/sh688001.day create mode 100644 tests/fixtures/sz000001.day create mode 100644 tests/fixtures/sz000001.lc5 create mode 100644 tests/test_reader.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0e1b0db..30ecddd 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 038c1d3..bbb42e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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(维护者) diff --git a/tests/fixtures/make_fixtures.py b/tests/fixtures/make_fixtures.py new file mode 100644 index 0000000..93dc573 --- /dev/null +++ b/tests/fixtures/make_fixtures.py @@ -0,0 +1,51 @@ +"""生成 tests/fixtures/ 下的二进制解析 fixture(issue #34)。 + +fixture 是确定性合成字节,格式与通达信实际文件逐字段一致: +- .day 32 字节/条:/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(' 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('z>% literal 0 HcmV?d00001 diff --git a/tests/fixtures/sz000001.lc5 b/tests/fixtures/sz000001.lc5 new file mode 100644 index 0000000000000000000000000000000000000000..bd8a9f8fd44bc8c672c5eac1e10781d07da30226 GIT binary patch literal 64 zcma#uWY5IFpy9~CpaaAzK&;`|pu^zhz{|`40WH1#OIAc>q3p B3Df`p literal 0 HcmV?d00001 diff --git a/tests/test_reader.py b/tests/test_reader.py new file mode 100644 index 0000000..03d326c --- /dev/null +++ b/tests/test_reader.py @@ -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