hikyuu 2.6.5__py3-none-win_amd64.whl → 2.6.7__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hikyuu/__init__.py +16 -0
- hikyuu/__init__.pyi +484 -474
- hikyuu/analysis/__init__.pyi +466 -445
- hikyuu/analysis/analysis.pyi +467 -446
- hikyuu/core.pyi +468 -447
- hikyuu/cpp/boost_date_time-mt.dll +0 -0
- hikyuu/cpp/boost_serialization-mt.dll +0 -0
- hikyuu/cpp/boost_wserialization-mt.dll +0 -0
- hikyuu/cpp/core310.pyd +0 -0
- hikyuu/cpp/core310.pyi +1070 -878
- hikyuu/cpp/core311.pyd +0 -0
- hikyuu/cpp/core311.pyi +1070 -878
- hikyuu/cpp/core312.pyd +0 -0
- hikyuu/cpp/core312.pyi +1070 -878
- hikyuu/cpp/core313.pyd +0 -0
- hikyuu/cpp/core313.pyi +1070 -876
- hikyuu/cpp/core39.pyd +0 -0
- hikyuu/cpp/core39.pyi +1070 -878
- hikyuu/cpp/hikyuu.dll +0 -0
- hikyuu/cpp/hikyuu.lib +0 -0
- hikyuu/cpp/i18n/__init__.py +0 -0
- hikyuu/cpp/i18n/zh_CN/__init__.py +0 -0
- hikyuu/cpp/i18n/zh_CN/hikyuu.mo +0 -0
- hikyuu/cpp/sqlite3.dll +0 -0
- hikyuu/data/clickhouse_upgrade/__init__.py +1 -0
- hikyuu/data/clickhouse_upgrade/createdb.sql +1085 -0
- hikyuu/data/common_clickhouse.py +465 -0
- hikyuu/data/em_block_to_clickhouse.py +120 -0
- hikyuu/data/hku_config_template.py +58 -3
- hikyuu/data/pytdx_finance_to_clickhouse.py +107 -0
- hikyuu/data/pytdx_to_clickhouse.py +841 -0
- hikyuu/data/pytdx_to_mysql.py +4 -4
- hikyuu/data/pytdx_weight_to_clickhouse.py +191 -0
- hikyuu/data/tdx_to_clickhouse.py +448 -0
- hikyuu/data/tdx_to_h5.py +1 -1
- hikyuu/data/zh_bond10_to_clickhouse.py +49 -0
- hikyuu/draw/__init__.pyi +1 -1
- hikyuu/draw/drawplot/bokeh_draw.pyi +479 -471
- hikyuu/draw/drawplot/echarts_draw.py +9 -8
- hikyuu/draw/drawplot/echarts_draw.pyi +479 -471
- hikyuu/draw/drawplot/matplotlib_draw.py +3 -3
- hikyuu/draw/drawplot/matplotlib_draw.pyi +479 -471
- hikyuu/draw/elder.pyi +6 -6
- hikyuu/draw/kaufman.py +1 -1
- hikyuu/draw/kaufman.pyi +10 -10
- hikyuu/draw/volume.pyi +5 -5
- hikyuu/examples/notebook/000-Index.ipynb +1 -1
- hikyuu/examples/notebook/001-overview.ipynb +78 -63
- hikyuu/examples/notebook/002-HowToGetStock.ipynb +259 -40
- hikyuu/examples/notebook/003-HowToGetKDataAndDraw.ipynb +49 -41
- hikyuu/examples/notebook/004-IndicatorOverview.ipynb +29 -29
- hikyuu/examples/notebook/005-Drawplot.ipynb +66 -37
- hikyuu/examples/notebook/006-TradeManager.ipynb +808 -61
- hikyuu/examples/notebook/007-SystemDetails.ipynb +23 -23
- hikyuu/examples/notebook/009-RealData.ipynb +3 -3
- hikyuu/examples/notebook/010-Portfolio.ipynb +761 -122
- hikyuu/extend.py +15 -100
- hikyuu/extend.pyi +478 -493
- hikyuu/fetcher/stock/zh_stock_a_pytdx.py +9 -20
- hikyuu/fetcher/stock/zh_stock_a_qmt.py +4 -5
- hikyuu/fetcher/stock/zh_stock_a_sina_qq.py +16 -60
- hikyuu/flat/Spot.py +96 -200
- hikyuu/gui/HikyuuTDX.py +134 -7
- hikyuu/gui/data/ImportBlockInfoTask.py +11 -0
- hikyuu/gui/data/ImportHistoryFinanceTask.py +15 -1
- hikyuu/gui/data/ImportPytdxTimeToH5Task.py +11 -1
- hikyuu/gui/data/ImportPytdxToH5Task.py +13 -1
- hikyuu/gui/data/ImportPytdxTransToH5Task.py +11 -1
- hikyuu/gui/data/ImportTdxToH5Task.py +13 -1
- hikyuu/gui/data/ImportWeightToSqliteTask.py +14 -1
- hikyuu/gui/data/ImportZhBond10Task.py +11 -0
- hikyuu/gui/data/MainWindow.py +210 -135
- hikyuu/gui/data/UsePytdxImportToH5Thread.py +45 -26
- hikyuu/gui/data/UseTdxImportToH5Thread.py +19 -1
- hikyuu/gui/dataserver.py +12 -4
- hikyuu/gui/spot_server.py +30 -40
- hikyuu/gui/start_qmt.py +20 -3
- hikyuu/hub.pyi +6 -6
- hikyuu/include/hikyuu/DataType.h +2 -0
- hikyuu/include/hikyuu/KQuery.h +22 -28
- hikyuu/include/hikyuu/MarketInfo.h +1 -1
- hikyuu/include/hikyuu/Stock.h +15 -3
- hikyuu/include/hikyuu/StockManager.h +12 -3
- hikyuu/include/hikyuu/StockTypeInfo.h +6 -0
- hikyuu/include/hikyuu/TransRecord.h +2 -8
- hikyuu/include/hikyuu/data_driver/kdata/mysql/KRecordTable.h +1 -0
- hikyuu/include/hikyuu/doc.h +4 -0
- hikyuu/include/hikyuu/global/GlobalSpotAgent.h +1 -1
- hikyuu/include/hikyuu/global/SpotRecord.h +15 -31
- hikyuu/include/hikyuu/global/agent/spot_generated.h +48 -232
- hikyuu/include/hikyuu/global/schedule/scheduler.h +1 -1
- hikyuu/include/hikyuu/indicator/Indicator.h +37 -0
- hikyuu/include/hikyuu/lang.h +27 -0
- hikyuu/include/hikyuu/plugin/KDataToHdf5Importer.h +9 -1
- hikyuu/include/hikyuu/plugin/dataserver.h +26 -1
- hikyuu/include/hikyuu/plugin/device.h +2 -1
- hikyuu/include/hikyuu/plugin/hkuextra.h +56 -0
- hikyuu/include/hikyuu/plugin/interface/DataDriverPluginInterface.h +27 -0
- hikyuu/include/hikyuu/plugin/interface/DataServerPluginInterface.h +2 -1
- hikyuu/include/hikyuu/plugin/interface/DevicePluginInterface.h +1 -1
- hikyuu/include/hikyuu/plugin/interface/HkuExtraPluginInterface.h +38 -0
- hikyuu/include/hikyuu/plugin/interface/ImportKDataToHdf5PluginInterface.h +13 -1
- hikyuu/include/hikyuu/plugin/interface/plugins.h +4 -0
- hikyuu/include/hikyuu/python/pybind_utils.h +9 -0
- hikyuu/include/hikyuu/strategy/Strategy.h +0 -9
- hikyuu/include/hikyuu/trade_manage/TradeRecord.h +1 -1
- hikyuu/include/hikyuu/utilities/config.h +0 -2
- hikyuu/include/hikyuu/utilities/os.h +9 -0
- hikyuu/include/hikyuu/utilities/plugin/PluginLoader.h +2 -1
- hikyuu/include/hikyuu/version.h +4 -4
- hikyuu/include/hikyuu/view/MarketView.h +59 -0
- hikyuu/indicator/__init__.py +0 -1
- hikyuu/indicator/indicator.py +14 -53
- hikyuu/plugin/backtest.dll +0 -0
- hikyuu/plugin/clickhousedriver.dll +0 -0
- hikyuu/plugin/dataserver.dll +0 -0
- hikyuu/plugin/device.dll +0 -0
- hikyuu/plugin/extind.dll +0 -0
- hikyuu/plugin/hkuextra.dll +0 -0
- hikyuu/plugin/import2hdf5.dll +0 -0
- hikyuu/plugin/tmreport.dll +0 -0
- hikyuu/test/test_init.py +59 -0
- hikyuu/test/test_real_tdx_import.py +336 -0
- hikyuu/test/test_tdx_import.py +315 -0
- hikyuu/test/test_tdx_real_data_import.py +281 -0
- hikyuu/trade_manage/__init__.pyi +479 -471
- hikyuu/trade_manage/trade.py +4 -65
- hikyuu/trade_manage/trade.pyi +479 -483
- hikyuu/trade_sys/__init__.py +11 -0
- hikyuu/util/__init__.py +1 -0
- hikyuu/util/__init__.pyi +4 -4
- hikyuu/util/check.py +8 -0
- hikyuu/util/check.pyi +5 -1
- hikyuu/util/singleton.pyi +1 -1
- {hikyuu-2.6.5.dist-info → hikyuu-2.6.7.dist-info}/METADATA +7 -5
- {hikyuu-2.6.5.dist-info → hikyuu-2.6.7.dist-info}/RECORD +142 -124
- {hikyuu-2.6.5.dist-info → hikyuu-2.6.7.dist-info}/top_level.txt +4 -3
- hikyuu/include/hikyuu/global/agent/hikyuu/flat/__init__.py +0 -1
- hikyuu/include/hikyuu/utilities/mo/__init__.py +0 -1
- hikyuu/include/hikyuu/utilities/mo/mo.h +0 -48
- hikyuu/indicator/talib_wrap.py +0 -1273
- /hikyuu/include/hikyuu/utilities/{mo/moFileReader.h → moFileReader.h} +0 -0
- /hikyuu/include/hikyuu/{global/agent/hikyuu → view}/__init__.py +0 -0
- {hikyuu-2.6.5.dist-info → hikyuu-2.6.7.dist-info}/LICENSE +0 -0
- {hikyuu-2.6.5.dist-info → hikyuu-2.6.7.dist-info}/WHEEL +0 -0
- {hikyuu-2.6.5.dist-info → hikyuu-2.6.7.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
测试模块:test_real_tdx_import.py
|
|
4
|
+
最后修改: 2025-08-07
|
|
5
|
+
|
|
6
|
+
本测试模块专用于验证通达信 (.day) 本地数据文件到 HDF5 格式的转换功能,
|
|
7
|
+
其核心特点是 **使用真实数据文件** 进行端到端的集成测试。
|
|
8
|
+
|
|
9
|
+
设计哲学:
|
|
10
|
+
- **真实性**: 直接使用从通达信客户端获取的 .day 文件, 确保测试场景尽可能贴近实际使用情况。
|
|
11
|
+
- **健壮性**: 提供了多种对比策略, 包括全量对比和交集对比, 以适应源数据可能存在清洗或范围差异的情况。
|
|
12
|
+
- **自动化**: 通过 Pytest Fixture 管理测试环境的创建和销毁, 并通过 `skipif` 自动跳过不满足前置条件的测试。
|
|
13
|
+
- **故障注入**: 包含一个故障注入测试, 确保断言逻辑本身是可靠的, 能够在数据不一致时准确地失败。
|
|
14
|
+
|
|
15
|
+
与其他测试的区别:
|
|
16
|
+
- `test_tdx_import.py`: 主要使用 **模拟数据** 和 **Mock对象** 对导入流程进行单元测试, 关注的是代码逻辑路径的正确性, 运行速度快且不依赖外部文件。
|
|
17
|
+
- `test_tdx_real_data_import.py`: 使用 **真实数据** 对核心转换函数 `tdx_import_day_data_from_file` 进行集成测试, 关注的是数据转换结果的准确性, 运行速度稍慢且依赖于一个真实的 .day 文件。
|
|
18
|
+
|
|
19
|
+
使用前提:
|
|
20
|
+
- 需要一个真实的通达信 .day 文件。测试默认会寻找环境变量 `HKU_real_tdx_file_path` 指定的文件,
|
|
21
|
+
如果未指定, 则会尝试读取 `/mnt/d1581/tdx/vipdoc/sh/lday/sh600000.day`。
|
|
22
|
+
如果文件不存在, 所有测试将被自动跳过。
|
|
23
|
+
"""
|
|
24
|
+
import os
|
|
25
|
+
import sqlite3
|
|
26
|
+
import struct
|
|
27
|
+
import tempfile
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
|
|
30
|
+
import pytest
|
|
31
|
+
import tables as tb
|
|
32
|
+
|
|
33
|
+
import unittest.mock
|
|
34
|
+
unittest.mock.patch('hikyuu.hku_cleanup', lambda: None).start()
|
|
35
|
+
|
|
36
|
+
from hikyuu.data.common_h5 import H5Record, get_h5table, open_h5file
|
|
37
|
+
# 假设要测试的函数位于以下模块
|
|
38
|
+
from hikyuu.data.tdx_to_h5 import tdx_import_day_data_from_file
|
|
39
|
+
from hikyuu.test.test_init import get_real_tdx_filepath
|
|
40
|
+
|
|
41
|
+
stcode = '600000' # 示例股票代码
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture(scope="module")
|
|
45
|
+
def real_tdx_file_path():
|
|
46
|
+
"""为当前模块的测试提供 sh600000.day 的路径"""
|
|
47
|
+
return get_real_tdx_filepath(stcode, market='sh')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# 用户提供的真实数据文件路径, 通过函数动态获取
|
|
51
|
+
# real_tdx_file_path = _get_real_tdx_filepath(stcode)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _read_raw_tdx_day_file(filename):
|
|
55
|
+
"""
|
|
56
|
+
一个辅助函数, 用于直接读取通达信官方的 .day 文件格式, 并将其解析为
|
|
57
|
+
一个与 HDF5 存储格式相匹配的字典列表, 以便后续进行数据对比。
|
|
58
|
+
"""
|
|
59
|
+
records = []
|
|
60
|
+
with open(filename, 'rb') as f:
|
|
61
|
+
while True:
|
|
62
|
+
chunk = f.read(32)
|
|
63
|
+
if not chunk:
|
|
64
|
+
break
|
|
65
|
+
if len(chunk) != 32:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
# 解析通达信.day文件格式 (32字节)
|
|
69
|
+
# 字段: date, open, high, low, close, amount, volume, reserved
|
|
70
|
+
# 类型: int, int, int, int, int, float, int, int
|
|
71
|
+
# 字节: 4, 4, 4, 4, 4, 4, 4, 4
|
|
72
|
+
raw_data = struct.unpack("iiiiifii", chunk)
|
|
73
|
+
|
|
74
|
+
# 核心逻辑: 必须与 tdx_import_day_data_from_file 中的数据清洗逻辑完全一致
|
|
75
|
+
# 如果开、高、低、收盘价中任何一个为0, 则该条记录被视为无效, 直接跳过
|
|
76
|
+
if 0 in raw_data[1:5]:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
# 将解析出的原始数据转换为与HDF5中存储格式完全一致的字典
|
|
80
|
+
record = {
|
|
81
|
+
'datetime': int(datetime.strptime(str(raw_data[0]), "%Y%m%d").strftime("%Y%m%d0000")),
|
|
82
|
+
'openPrice': raw_data[1] * 10,
|
|
83
|
+
'highPrice': raw_data[2] * 10,
|
|
84
|
+
'lowPrice': raw_data[3] * 10,
|
|
85
|
+
'closePrice': raw_data[4] * 10,
|
|
86
|
+
'transAmount': round(raw_data[5] * 0.001),
|
|
87
|
+
'transCount': round(raw_data[6] * 0.01)
|
|
88
|
+
}
|
|
89
|
+
records.append(record)
|
|
90
|
+
return records
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.fixture
|
|
94
|
+
def imported_data(request, real_tdx_file_path):
|
|
95
|
+
"""
|
|
96
|
+
一个 Module 级别的 Pytest Fixture, 它在当前测试模块的所有测试用例执行前,
|
|
97
|
+
仅运行一次。它负责:
|
|
98
|
+
1. 创建一个临时的 HDF5 文件和内存中的 SQLite 数据库。
|
|
99
|
+
2. 执行一次从 .day 文件到 HDF5 的导入操作。
|
|
100
|
+
3. 使用 `yield` 将导入的结果 (如HDF5文件句柄、导入计数等) 提供给所有测试用例。
|
|
101
|
+
4. 在所有测试用例执行完毕后, 自动清理临时文件和数据库连接。
|
|
102
|
+
|
|
103
|
+
这种方式可以避免每个测试都重复执行耗时的导入操作, 显著提高测试效率。
|
|
104
|
+
"""
|
|
105
|
+
# 1. 创建临时的数据库和数据目录
|
|
106
|
+
db_connect = sqlite3.connect(":memory:")
|
|
107
|
+
cur = db_connect.cursor()
|
|
108
|
+
cur.execute(
|
|
109
|
+
'''CREATE TABLE stock (
|
|
110
|
+
stockid INTEGER PRIMARY KEY, marketid INTEGER, code TEXT,
|
|
111
|
+
valid INTEGER, type INTEGER, startdate INTEGER, enddate INTEGER
|
|
112
|
+
)'''
|
|
113
|
+
)
|
|
114
|
+
cur.execute(
|
|
115
|
+
f"INSERT INTO stock (stockid, marketid, code, valid, type, startdate, enddate) VALUES (1, 1, '{stcode}', 1, 1, 0, 99999999)"
|
|
116
|
+
)
|
|
117
|
+
cur.execute("CREATE TABLE market (marketid INTEGER, lastdate INTEGER)")
|
|
118
|
+
cur.execute("INSERT INTO market (marketid, lastdate) VALUES (1, 0)") # lastdate=0 确保从头导入
|
|
119
|
+
db_connect.commit()
|
|
120
|
+
|
|
121
|
+
temp_dir = tempfile.TemporaryDirectory()
|
|
122
|
+
data_dir = temp_dir.name
|
|
123
|
+
|
|
124
|
+
# 2. 执行导入
|
|
125
|
+
market = 'SH'
|
|
126
|
+
stock_code = stcode
|
|
127
|
+
cur.execute(f"SELECT stockid, marketid, code, valid, type FROM stock WHERE code = '{stock_code}'")
|
|
128
|
+
stock_record = cur.fetchone()
|
|
129
|
+
h5file = open_h5file(data_dir, market, 'DAY')
|
|
130
|
+
|
|
131
|
+
imported_count = tdx_import_day_data_from_file(
|
|
132
|
+
connect=db_connect, filename=real_tdx_file_path, h5file=h5file, market=market, stock_record=stock_record
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# 3. 使用 yield 将必要信息打包返回给测试用例
|
|
136
|
+
yield {
|
|
137
|
+
"h5file": h5file,
|
|
138
|
+
"market": market,
|
|
139
|
+
"stock_code": stock_code,
|
|
140
|
+
"imported_count": imported_count,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# 4. 测试用例执行完毕后, 清理资源
|
|
144
|
+
h5file.close()
|
|
145
|
+
temp_dir.cleanup()
|
|
146
|
+
db_connect.close()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# 使用 pytest.mark.skipif 装饰器, 如果指定的条件为真, 则跳过该测试。
|
|
150
|
+
# 这里用于检查真实的 .day 文件是否存在, 如果不存在, 测试将不会执行, 并给出友好提示。
|
|
151
|
+
def test_import_and_verify_output(imported_data, real_tdx_file_path):
|
|
152
|
+
if not os.path.exists(real_tdx_file_path):
|
|
153
|
+
pytest.skip(f"TDX file not found: {real_tdx_file_path}")
|
|
154
|
+
"""
|
|
155
|
+
测试目标: 验证导入流程能成功执行, 并对导入结果进行一次快速的、人工友好的抽样检查。
|
|
156
|
+
测试方法:
|
|
157
|
+
- 检查导入的记录数是否大于0。
|
|
158
|
+
- 打印 HDF5 文件中的第一条和最后一条记录的关键字段。
|
|
159
|
+
预期结果:
|
|
160
|
+
- 测试通过, 并在控制台输出清晰的首末记录, 方便人工核对。
|
|
161
|
+
"""
|
|
162
|
+
print(f"\n成功从 {os.path.basename(real_tdx_file_path)} 导入了 {imported_data['imported_count']} 条记录。")
|
|
163
|
+
assert imported_data['imported_count'] > 0
|
|
164
|
+
|
|
165
|
+
table = get_h5table(imported_data['h5file'], imported_data['market'], imported_data['stock_code'])
|
|
166
|
+
print("\n请核对以下数据是否正确:")
|
|
167
|
+
print("=" * 30)
|
|
168
|
+
|
|
169
|
+
first_record = table[0]
|
|
170
|
+
print(f"第一条记录: ")
|
|
171
|
+
print(f" 日期 (datetime): {first_record['datetime']}")
|
|
172
|
+
print(f" 开盘价 (open): {first_record['openPrice'] / 1000.0}")
|
|
173
|
+
print(f" 最高价 (high): {first_record['highPrice'] / 1000.0}")
|
|
174
|
+
print(f" 最低价 (low): {first_record['lowPrice'] / 1000.0}")
|
|
175
|
+
print(f" 收盘价 (close): {first_record['closePrice'] / 1000.0}")
|
|
176
|
+
print(f" 成交额 (amount): {first_record['transAmount'] / 1000.0}")
|
|
177
|
+
print(f" 成交量 (volume): {first_record['transCount']}")
|
|
178
|
+
|
|
179
|
+
last_record = table[-1]
|
|
180
|
+
print(f"最后一条记录: ")
|
|
181
|
+
print(f" 日期 (datetime): {last_record['datetime']}")
|
|
182
|
+
print(f" 开盘价 (open): {last_record['openPrice'] / 1000.0}")
|
|
183
|
+
print(f" 最高价 (high): {last_record['highPrice'] / 1000.0}")
|
|
184
|
+
print(f" 最低价 (low): {last_record['lowPrice'] / 1000.0}")
|
|
185
|
+
print(f" 收盘价 (close): {last_record['closePrice'] / 1000.0}")
|
|
186
|
+
print(f" 成交额 (amount): {last_record['transAmount'] / 1000.0}")
|
|
187
|
+
print(f" 成交量 (volume): {last_record['transCount']}")
|
|
188
|
+
|
|
189
|
+
print("-" * 30)
|
|
190
|
+
print(f"共有 {len(table)} 条记录。")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_compare_imported_data_with_source(imported_data, real_tdx_file_path):
|
|
194
|
+
if not os.path.exists(real_tdx_file_path):
|
|
195
|
+
pytest.skip(f"TDX file not found: {real_tdx_file_path}")
|
|
196
|
+
"""
|
|
197
|
+
测试目标: 对比 HDF5 中的数据和原始 .day 文件的数据, 确保 **全量数据** 的精确一致性。
|
|
198
|
+
测试方法:
|
|
199
|
+
- 分别读取 HDF5 和 .day 文件中的所有记录。
|
|
200
|
+
- 首先断言两者记录数完全相等。
|
|
201
|
+
- 然后逐条、逐字段地进行精确断言。
|
|
202
|
+
预期结果:
|
|
203
|
+
- 只有在两条数据源完全一致的情况下, 测试才能通过。
|
|
204
|
+
- 如果导入过程中存在任何数据清洗或过滤, 此测试将会失败。
|
|
205
|
+
"""
|
|
206
|
+
# 1. 从HDF5文件读取已导入的数据
|
|
207
|
+
h5_table = get_h5table(imported_data['h5file'], imported_data['market'], imported_data['stock_code'])
|
|
208
|
+
|
|
209
|
+
# 2. 从原始.day文件直接读取数据
|
|
210
|
+
raw_records = _read_raw_tdx_day_file(real_tdx_file_path)
|
|
211
|
+
|
|
212
|
+
# 3. 断言记录数量一致
|
|
213
|
+
assert len(h5_table) == len(raw_records), "导入后的记录数与源文件不一致!"
|
|
214
|
+
|
|
215
|
+
# 4. 逐条比较记录
|
|
216
|
+
for i, h5_record in enumerate(h5_table):
|
|
217
|
+
raw_record = raw_records[i]
|
|
218
|
+
assert h5_record['datetime'] == raw_record['datetime'], f"第 {i+1} 条记录的日期不匹配"
|
|
219
|
+
assert h5_record['openPrice'] == raw_record['openPrice'], f"第 {i+1} 条记录的开盘价不匹配"
|
|
220
|
+
assert h5_record['highPrice'] == raw_record['highPrice'], f"第 {i+1} 条记录的最高价不匹配"
|
|
221
|
+
assert h5_record['lowPrice'] == raw_record['lowPrice'], f"第 {i+1} 条记录的最低价不匹配"
|
|
222
|
+
assert h5_record['closePrice'] == raw_record['closePrice'], f"第 {i+1} 条记录的收盘价不匹配"
|
|
223
|
+
assert h5_record['transAmount'] == raw_record['transAmount'], f"第 {i+1} 条记录的成交额不匹配"
|
|
224
|
+
assert h5_record['transCount'] == raw_record['transCount'], f"第 {i+1} 条记录的成交量不匹配"
|
|
225
|
+
|
|
226
|
+
print(f"\n成功校验 {len(h5_table)} 条记录,HDF5数据与源文件完全一致。")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_compare_common_data_with_source(imported_data, real_tdx_file_path):
|
|
230
|
+
if not os.path.exists(real_tdx_file_path):
|
|
231
|
+
pytest.skip(f"TDX file not found: {real_tdx_file_path}")
|
|
232
|
+
"""
|
|
233
|
+
测试目标: 对比 HDF5 和 .day 文件的数据, 确保 **交集部分** 的数据精确一致性。
|
|
234
|
+
测试方法:
|
|
235
|
+
- 将两个数据源都读入以日期为键的字典中。
|
|
236
|
+
- 找出两个日期集合的交集。
|
|
237
|
+
- 只对交集中的数据进行逐条、逐字段的精确断言。
|
|
238
|
+
预期结果:
|
|
239
|
+
- 即使两个数据源的记录总数不同 (例如, HDF5中清洗了部分无效数据),
|
|
240
|
+
只要它们共有的那部分数据是完全一致的, 测试就能通过。
|
|
241
|
+
- 这是一个比全量对比更健壮的测试。
|
|
242
|
+
"""
|
|
243
|
+
# 1. 从HDF5文件读取数据并转换为以日期为键的字典, 方便快速查找
|
|
244
|
+
h5_table = get_h5table(imported_data['h5file'], imported_data['market'], imported_data['stock_code'])
|
|
245
|
+
h5_records_map = {row['datetime']: {col: row[col] for col in h5_table.colnames} for row in h5_table.iterrows()}
|
|
246
|
+
|
|
247
|
+
# 2. 从原始.day文件直接读取数据并也转换为字典
|
|
248
|
+
raw_records_list = _read_raw_tdx_day_file(real_tdx_file_path)
|
|
249
|
+
raw_records_map = {row['datetime']: row for row in raw_records_list}
|
|
250
|
+
|
|
251
|
+
# 3. 打印数据源信息, 方便调试
|
|
252
|
+
print("\n--- 数据源信息 ---")
|
|
253
|
+
if h5_records_map:
|
|
254
|
+
h5_dates_list = sorted(h5_records_map.keys())
|
|
255
|
+
print(f"HDF5 : 共 {len(h5_records_map)} 条记录, 时间范围: {h5_dates_list[0]} -> {h5_dates_list[-1]}")
|
|
256
|
+
else:
|
|
257
|
+
print("HDF5 : 0 条记录")
|
|
258
|
+
|
|
259
|
+
if raw_records_map:
|
|
260
|
+
raw_dates_list = sorted(raw_records_map.keys())
|
|
261
|
+
print(f"原始.day文件: 共 {len(raw_records_map)} 条记录, 时间范围: {raw_dates_list[0]} -> {raw_dates_list[-1]}")
|
|
262
|
+
else:
|
|
263
|
+
print("原始.day文件: 0 条记录")
|
|
264
|
+
|
|
265
|
+
# 4. 寻找两个日期集合的交集, 并进行比较
|
|
266
|
+
h5_dates = set(h5_records_map.keys())
|
|
267
|
+
raw_dates = set(raw_records_map.keys())
|
|
268
|
+
common_dates = sorted(list(h5_dates.intersection(raw_dates)))
|
|
269
|
+
|
|
270
|
+
print(f"共同部分 : 共 {len(common_dates)} 条记录用于比较。")
|
|
271
|
+
print("--------------------")
|
|
272
|
+
|
|
273
|
+
assert len(common_dates) > 0, "HDF5文件与原始文件没有共同的数据可供比较!"
|
|
274
|
+
|
|
275
|
+
# 5. 逐条比较共同的记录
|
|
276
|
+
for date in common_dates:
|
|
277
|
+
h5_record = h5_records_map[date]
|
|
278
|
+
raw_record = raw_records_map[date]
|
|
279
|
+
|
|
280
|
+
assert h5_record['openPrice'] == raw_record['openPrice'], f"日期 {date} 的开盘价不匹配"
|
|
281
|
+
assert h5_record['highPrice'] == raw_record['highPrice'], f"日期 {date} 的最高价不匹配"
|
|
282
|
+
assert h5_record['lowPrice'] == raw_record['lowPrice'], f"日期 {date} 的最低价不匹配"
|
|
283
|
+
assert h5_record['closePrice'] == raw_record['closePrice'], f"日期 {date} 的收盘价不匹配"
|
|
284
|
+
assert h5_record['transAmount'] == raw_record['transAmount'], f"日期 {date} 的成交额不匹配"
|
|
285
|
+
assert h5_record['transCount'] == raw_record['transCount'], f"日期 {date} 的成交量不匹配"
|
|
286
|
+
|
|
287
|
+
print(f"\n成功校验 {len(common_dates)} 条共同记录,数据完全一致。")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_mismatched_data_is_detected(imported_data, real_tdx_file_path):
|
|
291
|
+
if not os.path.exists(real_tdx_file_path):
|
|
292
|
+
pytest.skip(f"TDX file not found: {real_tdx_file_path}")
|
|
293
|
+
"""
|
|
294
|
+
测试目标: 验证我们的对比断言逻辑是可靠的, 能够在数据不一致时 **按预期失败**。
|
|
295
|
+
测试方法 (故障注入):
|
|
296
|
+
- 正常读取两份数据。
|
|
297
|
+
- 在对比前, **故意修改** 原始数据中的一条记录 (例如, 修改开盘价)。
|
|
298
|
+
- 使用 `pytest.raises(AssertionError)` 上下文管理器来执行对比逻辑。
|
|
299
|
+
预期结果:
|
|
300
|
+
- 对比逻辑在执行到被修改的那条数据时, 应该抛出 `AssertionError`。
|
|
301
|
+
- `pytest.raises` 会捕获这个异常, 从而让整个测试用例 **通过**。
|
|
302
|
+
- 如果对比逻辑有误, 没有抛出异常, `pytest.raises` 会反过来让测试 **失败**。
|
|
303
|
+
- 同时, 还会检查捕获到的异常信息, 确保它是由我们注入的故障导致的。
|
|
304
|
+
"""
|
|
305
|
+
# 1. 准备数据
|
|
306
|
+
h5_table = get_h5table(imported_data['h5file'], imported_data['market'], imported_data['stock_code'])
|
|
307
|
+
h5_records_map = {row['datetime']: {col: row[col] for col in h5_table.colnames} for row in h5_table.iterrows()}
|
|
308
|
+
raw_records_list = _read_raw_tdx_day_file(real_tdx_file_path)
|
|
309
|
+
raw_records_map = {row['datetime']: row for row in raw_records_list}
|
|
310
|
+
common_dates = sorted(list(set(h5_records_map.keys()).intersection(set(raw_records_map.keys()))))
|
|
311
|
+
|
|
312
|
+
assert len(common_dates) > 100, "数据量太少,无法执行本测试"
|
|
313
|
+
|
|
314
|
+
# 2. 故意修改一条数据以注入故障
|
|
315
|
+
target_date = common_dates[100] # 选择一条记录进行修改
|
|
316
|
+
original_price = raw_records_map[target_date]['openPrice']
|
|
317
|
+
modified_price = 99999999
|
|
318
|
+
raw_records_map[target_date]['openPrice'] = modified_price
|
|
319
|
+
|
|
320
|
+
print(f"\n[故障注入] 将日期 {target_date} 的原始开盘价 ({original_price}) 修改为 {modified_price} 以测试断言。\n")
|
|
321
|
+
|
|
322
|
+
# 3. 使用 pytest.raises 验证断言是否会按预期失败
|
|
323
|
+
with pytest.raises(AssertionError) as excinfo:
|
|
324
|
+
# 复用之前的比较逻辑
|
|
325
|
+
for date in common_dates:
|
|
326
|
+
h5_record = h5_records_map[date]
|
|
327
|
+
raw_record = raw_records_map[date]
|
|
328
|
+
assert h5_record['openPrice'] == raw_record['openPrice'], f"日期 {date} 的开盘价不匹配"
|
|
329
|
+
# ... (其他字段的断言在此处可以省略, 因为我们预期在 openPrice 就会失败)
|
|
330
|
+
|
|
331
|
+
# 4. 验证失败信息是否包含了我们期望的内容, 确保断言是在正确的位置失败的
|
|
332
|
+
error_message = str(excinfo.value)
|
|
333
|
+
print(f"[断言验证] 捕获到错误: {error_message}")
|
|
334
|
+
assert str(target_date) in error_message
|
|
335
|
+
assert "开盘价不匹配" in error_message
|
|
336
|
+
print("[验证成功] 断言已在正确的位置因正确的原因失败。")
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
测试模块: test_tdx_import.py
|
|
4
|
+
|
|
5
|
+
本测试模块使用 unittest.TestCase 结构, 旨在验证通达信(TDX)本地数据
|
|
6
|
+
导入HDF5文件的核心流程。
|
|
7
|
+
|
|
8
|
+
由于导入过程涉及到多进程、文件I/O和数据库操作, 为了实现一个稳定、可靠的
|
|
9
|
+
单元测试, 本模块采取了以下策略:
|
|
10
|
+
1. **环境隔离**: 使用 unittest 的 setUp 和 tearDown 方法, 在系统的临时
|
|
11
|
+
目录中为每个测试创建独立、干净的运行环境, 并在测试结束后自动清理,
|
|
12
|
+
避免对开发环境造成污染。
|
|
13
|
+
2. **依赖模拟 (Mocking)**: 通过 unittest.mock.patch, 模拟了所有外部
|
|
14
|
+
依赖, 包括:
|
|
15
|
+
- 数据库初始化 (sqlite_create_database)
|
|
16
|
+
- 网络数据获取 (search_best_tdx, TdxHq_API)
|
|
17
|
+
- 其他数据导入函数 (sqlite_import_stock_name 等)
|
|
18
|
+
这样可以确保测试只关注于导入逻辑本身, 不受网络状况或外部API变动的影响。
|
|
19
|
+
3. **同步执行**: 导入线程 (UseTdxImportToH5Thread) 的多进程执行逻辑
|
|
20
|
+
(_run 方法) 被一个自定义的内部测试类 (TestImporterThread) 覆盖,
|
|
21
|
+
将异步的多进程操作转换为同步的单进程操作。这彻底解决了在测试过程中
|
|
22
|
+
因进程间文件读写延迟而导致的"竞争条件" (Race Condition) 问题,
|
|
23
|
+
是确保测试稳定性的关键。
|
|
24
|
+
"""
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
import time
|
|
28
|
+
import struct
|
|
29
|
+
import unittest
|
|
30
|
+
import sqlite3
|
|
31
|
+
import tempfile
|
|
32
|
+
import shutil
|
|
33
|
+
from configparser import ConfigParser
|
|
34
|
+
from unittest.mock import patch, MagicMock
|
|
35
|
+
|
|
36
|
+
# 确保项目根目录在Python的模块搜索路径中, 以便正确导入hikyuu库
|
|
37
|
+
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
38
|
+
if project_root not in sys.path:
|
|
39
|
+
sys.path.insert(0, project_root)
|
|
40
|
+
|
|
41
|
+
from hikyuu.gui.data.UseTdxImportToH5Thread import UseTdxImportToH5Thread
|
|
42
|
+
from hikyuu.gui.data.ImportTdxToH5Task import ImportTdxToH5Task
|
|
43
|
+
|
|
44
|
+
# 引入 get_real_tdx_filepath, 用于支持使用真实的测试数据
|
|
45
|
+
_current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
46
|
+
if _current_dir not in sys.path:
|
|
47
|
+
sys.path.append(_current_dir)
|
|
48
|
+
from test_init import get_real_tdx_filepath
|
|
49
|
+
|
|
50
|
+
# ------------------------------------------------------------------------------
|
|
51
|
+
# Test Helper Classes
|
|
52
|
+
# ------------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
class MockRunner:
|
|
55
|
+
"""
|
|
56
|
+
模拟 PyQt 的信号接收器 (Slot), 用于在非GUI环境中捕获并记录导入线程
|
|
57
|
+
发出的完成信号。这是连接测试主线程和导入线程状态的关键。
|
|
58
|
+
"""
|
|
59
|
+
def __init__(self):
|
|
60
|
+
"""初始化状态,标记导入任务尚未完成。"""
|
|
61
|
+
self.finished = False
|
|
62
|
+
|
|
63
|
+
def on_message_from_thread(self, msg):
|
|
64
|
+
"""
|
|
65
|
+
这是一个模拟的槽函数, 用于接收信号。当接收到表示任务结束或失败的
|
|
66
|
+
特定消息时, 将 finished 标志位设为 True。
|
|
67
|
+
"""
|
|
68
|
+
# 检查消息是否为表示任务结束的特定格式: ['HDF5_IMPORT', 'THREAD', 'FINISHED' 或 'FAILURE']
|
|
69
|
+
if msg and len(msg) > 2 and msg[0] == 'HDF5_IMPORT' and msg[1] == 'THREAD' and msg[2] in ('FINISHED', 'FAILURE'):
|
|
70
|
+
self.finished = True
|
|
71
|
+
|
|
72
|
+
class MockSignal:
|
|
73
|
+
"""
|
|
74
|
+
模拟 PyQt 的 pyqtSignal, 允许在非GUI的测试环境中"发送"信号。
|
|
75
|
+
它通过直接调用已连接的处理器 (handler) 来实现。
|
|
76
|
+
"""
|
|
77
|
+
def __init__(self, handler):
|
|
78
|
+
"""
|
|
79
|
+
初始化信号, 将其与一个处理器 (如 MockRunner.on_message_from_thread) 绑定。
|
|
80
|
+
"""
|
|
81
|
+
self.handler = handler
|
|
82
|
+
|
|
83
|
+
def emit(self, msg):
|
|
84
|
+
"""模拟发送信号, 实际是直接调用绑定的处理器函数。"""
|
|
85
|
+
self.handler(msg)
|
|
86
|
+
|
|
87
|
+
def create_dummy_tdx_day_file(filepath, data):
|
|
88
|
+
"""
|
|
89
|
+
创建一个符合通达信官方 .day 文件格式的二进制虚拟文件。
|
|
90
|
+
这使得测试不依赖于真实的、可能变动的TDX客户端数据。
|
|
91
|
+
"""
|
|
92
|
+
# 结构体格式:
|
|
93
|
+
# < : 小端字节序 (Little-endian)
|
|
94
|
+
# I : unsigned int (4字节), 用于日期和成交量
|
|
95
|
+
# i : int (4字节), 用于价格 (开、高、低、收)
|
|
96
|
+
# f : float (4字节), 用于成交金额
|
|
97
|
+
# I : unsigned int (4字节), 用于保留字段
|
|
98
|
+
record = struct.pack("<IiiiifII", data['date'], int(data['open'] * 100), int(data['high'] * 100),
|
|
99
|
+
int(data['low'] * 100), int(data['close'] * 100), data['amount'],
|
|
100
|
+
data['volume'], 0)
|
|
101
|
+
with open(filepath, 'wb') as f:
|
|
102
|
+
f.write(record)
|
|
103
|
+
|
|
104
|
+
# ------------------------------------------------------------------------------
|
|
105
|
+
# Main Test Class
|
|
106
|
+
# ------------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
class TestTdxImport(unittest.TestCase):
|
|
109
|
+
"""
|
|
110
|
+
封装了所有针对TDX本地数据导入HDF5功能的测试用例。
|
|
111
|
+
通过 setUp 和 tearDown 方法确保每个测试都在独立、干净的环境中运行。
|
|
112
|
+
"""
|
|
113
|
+
def setUp(self):
|
|
114
|
+
"""
|
|
115
|
+
在每个测试用例执行前, 初始化一个全新的、隔离的测试环境。
|
|
116
|
+
这包括创建临时目录、模拟的TDX数据文件、HDF5目标目录以及一个
|
|
117
|
+
包含必要元数据的SQLite数据库。
|
|
118
|
+
"""
|
|
119
|
+
# 1. 创建一个唯一的临时目录, 避免测试间干扰和对本地环境的污染
|
|
120
|
+
self.temp_dir = tempfile.mkdtemp(prefix="test_tdx_import_")
|
|
121
|
+
|
|
122
|
+
# 2. 在临时目录中创建通达信源数据和HDF5目标数据的目录结构
|
|
123
|
+
self.tdx_source_dir = os.path.join(self.temp_dir, "tdx_source")
|
|
124
|
+
self.hdf5_dest_dir = os.path.join(self.temp_dir, "hdf5_dest")
|
|
125
|
+
sh_dir = os.path.join(self.tdx_source_dir, "vipdoc", "sh", "lday")
|
|
126
|
+
os.makedirs(sh_dir)
|
|
127
|
+
os.makedirs(self.hdf5_dest_dir)
|
|
128
|
+
|
|
129
|
+
# 3. 创建一个虚拟的通达信 .day 文件, 作为导入的数据源
|
|
130
|
+
# 如果找到真实的通达信文件, 则优先使用, 否则创建虚拟文件
|
|
131
|
+
dest_day_file = os.path.join(sh_dir, 'sh600000.day')
|
|
132
|
+
real_day_file = get_real_tdx_filepath(code='600000', market='sh')
|
|
133
|
+
if os.path.exists(real_day_file):
|
|
134
|
+
shutil.copy(real_day_file, dest_day_file)
|
|
135
|
+
else:
|
|
136
|
+
dummy_data_sh = {'date': 20230103, 'open': 10.0, 'high': 11.5, 'low': 9.8, 'close': 11.2, 'amount': 550000.0, 'volume': 50000}
|
|
137
|
+
create_dummy_tdx_day_file(dest_day_file, dummy_data_sh)
|
|
138
|
+
|
|
139
|
+
# 4. 创建一个导入任务必需的、结构完整、内容正确的SQLite数据库
|
|
140
|
+
# 这个数据库为导入过程提供了股票代码和市场等元数据。
|
|
141
|
+
db_path = os.path.join(self.hdf5_dest_dir, "stock.db")
|
|
142
|
+
con = sqlite3.connect(db_path)
|
|
143
|
+
cur = con.cursor()
|
|
144
|
+
cur.execute("CREATE TABLE market (marketid INTEGER, market TEXT, name TEXT, lastdate INTEGER)")
|
|
145
|
+
cur.execute("INSERT INTO market VALUES (1, 'SH', '上海证券交易所', 0)")
|
|
146
|
+
cur.execute("CREATE TABLE stock (stockid INTEGER, marketid INTEGER, code TEXT, valid INTEGER, type INTEGER, name TEXT, startdate INTEGER, enddate INTEGER)")
|
|
147
|
+
cur.execute("INSERT INTO stock VALUES (1, 1, '600000', 1, 1, '浦发银行', 19991110, 99999999)")
|
|
148
|
+
con.commit()
|
|
149
|
+
con.close()
|
|
150
|
+
|
|
151
|
+
# 5. 创建一个指向临时环境的配置对象 (ConfigParser)
|
|
152
|
+
# 这个配置在运行时传递给导入线程, 指导其在哪里找到源文件和存放目标文件。
|
|
153
|
+
self.config = ConfigParser()
|
|
154
|
+
self.config.add_section('hdf5'); self.config.set('hdf5', 'enable', 'True'); self.config.set('hdf5', 'dir', self.hdf5_dest_dir)
|
|
155
|
+
self.config.add_section('tdx'); self.config.set('tdx', 'enable', 'True'); self.config.set('tdx', 'dir', self.tdx_source_dir)
|
|
156
|
+
self.config.add_section('ktype'); self.config.set('ktype', 'day', 'True'); self.config.set('ktype', 'min', 'False')
|
|
157
|
+
self.config.add_section('quotation'); self.config.set('quotation', 'stock', 'True'); self.config.set('quotation', 'fund', 'False')
|
|
158
|
+
self.config.add_section('weight'); self.config.set('weight', 'enable', 'False');
|
|
159
|
+
self.config.add_section('finance'); self.config.set('finance', 'enable', 'False')
|
|
160
|
+
self.config.add_section('pytdx'); self.config.set('pytdx', 'enable', 'False')
|
|
161
|
+
|
|
162
|
+
def tearDown(self):
|
|
163
|
+
"""
|
|
164
|
+
在每个测试用例执行后, 无论成功与否, 都会彻底清理临时目录和其中的所有文件,
|
|
165
|
+
确保测试环境的纯净性。
|
|
166
|
+
"""
|
|
167
|
+
shutil.rmtree(self.temp_dir)
|
|
168
|
+
|
|
169
|
+
@patch('hikyuu.gui.data.UseTdxImportToH5Thread.TdxHq_API')
|
|
170
|
+
@patch('hikyuu.gui.data.UseTdxImportToH5Thread.sqlite_import_new_holidays')
|
|
171
|
+
@patch('hikyuu.gui.data.UseTdxImportToH5Thread.sqlite_import_index_name')
|
|
172
|
+
@patch('hikyuu.gui.data.UseTdxImportToH5Thread.sqlite_import_stock_name')
|
|
173
|
+
@patch('hikyuu.gui.data.UseTdxImportToH5Thread.sqlite_create_database')
|
|
174
|
+
@patch('hikyuu.gui.data.UseTdxImportToH5Thread.search_best_tdx')
|
|
175
|
+
def test_import_process_completes(self, mock_search, mock_create, mock_stock, mock_index, mock_holiday, mock_api):
|
|
176
|
+
"""
|
|
177
|
+
冒烟测试 (Smoke Test): 验证导入流程可以被完整触发, 并且能够正常运行结束,
|
|
178
|
+
最终发出 'FINISHED' 信号, 全程不发生崩溃或死锁。
|
|
179
|
+
|
|
180
|
+
此测试的核心是验证流程的稳定性, 不关心导入数据的具体内容是否正确。
|
|
181
|
+
通过模拟所有外部依赖, 确保测试的焦点仅在代码的执行路径上。
|
|
182
|
+
"""
|
|
183
|
+
# 定义一个自定义的导入线程类, 其唯一目的是覆盖_run方法以实现同步执行
|
|
184
|
+
class TestImporterThread(UseTdxImportToH5Thread):
|
|
185
|
+
def _run(self):
|
|
186
|
+
# 在测试中直接、同步地运行导入任务, 避免多进程带来的复杂性和竞争条件
|
|
187
|
+
for task in self.tasks:
|
|
188
|
+
if isinstance(task, ImportTdxToH5Task) and task.market == 'SH':
|
|
189
|
+
task()
|
|
190
|
+
# 任务完成后, 发送结束信号
|
|
191
|
+
self.send_message(['THREAD', 'FINISHED'])
|
|
192
|
+
|
|
193
|
+
# 配置mock, 模拟 search_best_tdx 返回符合其真实数据结构的虚拟服务器地址
|
|
194
|
+
mock_search.return_value = [(True, 0.0, '127.0.0.1', 7709)]
|
|
195
|
+
|
|
196
|
+
# 配置mock, 模拟 TdxHq_API 的行为
|
|
197
|
+
mock_api_instance = MagicMock()
|
|
198
|
+
mock_api_instance.connect.return_value = True
|
|
199
|
+
mock_api.return_value = mock_api_instance
|
|
200
|
+
|
|
201
|
+
# 实例化各个模拟组件和我们自定义的测试导入器
|
|
202
|
+
runner = MockRunner()
|
|
203
|
+
importer = TestImporterThread(None, self.config)
|
|
204
|
+
importer.message = MockSignal(runner.on_message_from_thread)
|
|
205
|
+
|
|
206
|
+
# 启动导入流程
|
|
207
|
+
importer.run()
|
|
208
|
+
|
|
209
|
+
# 等待导入结束信号, 设置10秒超时以防止测试在失败时永久挂起
|
|
210
|
+
start_time = time.time()
|
|
211
|
+
while not runner.finished and time.time() - start_time < 10:
|
|
212
|
+
time.sleep(0.01)
|
|
213
|
+
|
|
214
|
+
# 断言: 确认导入过程已正常结束
|
|
215
|
+
self.assertTrue(runner.finished, "导入过程未能按预期在10秒内发出完成信号, 可能发生死锁或意外错误。")
|
|
216
|
+
|
|
217
|
+
@patch('hikyuu.gui.data.UseTdxImportToH5Thread.TdxHq_API')
|
|
218
|
+
@patch('hikyuu.gui.data.UseTdxImportToH5Thread.sqlite_import_new_holidays')
|
|
219
|
+
@patch('hikyuu.gui.data.UseTdxImportToH5Thread.sqlite_import_index_name')
|
|
220
|
+
@patch('hikyuu.gui.data.UseTdxImportToH5Thread.sqlite_import_stock_name')
|
|
221
|
+
@patch('hikyuu.gui.data.UseTdxImportToH5Thread.sqlite_create_database')
|
|
222
|
+
@patch('hikyuu.gui.data.UseTdxImportToH5Thread.search_best_tdx')
|
|
223
|
+
def test_import_data_verification(self, mock_search, mock_create, mock_stock, mock_index, mock_holiday, mock_api):
|
|
224
|
+
"""
|
|
225
|
+
核心功能测试: 验证从模拟的 .day 文件导入到HDF5文件中的数据,
|
|
226
|
+
其数值和结构是否完全正确。这是确保导入逻辑正确性的关键测试。
|
|
227
|
+
"""
|
|
228
|
+
try:
|
|
229
|
+
import h5py
|
|
230
|
+
import numpy as np
|
|
231
|
+
except ImportError:
|
|
232
|
+
self.skipTest("h5py 或 numpy 未安装, 跳过此核心功能测试")
|
|
233
|
+
|
|
234
|
+
# 同样地, 定义一个用于测试的同步导入线程
|
|
235
|
+
class TestImporterThread(UseTdxImportToH5Thread):
|
|
236
|
+
def _run(self):
|
|
237
|
+
for task in self.tasks:
|
|
238
|
+
if isinstance(task, ImportTdxToH5Task) and task.market == 'SH':
|
|
239
|
+
task()
|
|
240
|
+
self.send_message(['THREAD', 'FINISHED'])
|
|
241
|
+
|
|
242
|
+
# 配置mock, 模拟 search_best_tdx 返回符合其真实数据结构的虚拟服务器地址
|
|
243
|
+
mock_search.return_value = [(True, 0.0, '127.0.0.1', 7709)]
|
|
244
|
+
|
|
245
|
+
# 配置mock, 模拟 TdxHq_API 的行为
|
|
246
|
+
mock_api_instance = MagicMock()
|
|
247
|
+
mock_api_instance.connect.return_value = True
|
|
248
|
+
mock_api.return_value = mock_api_instance
|
|
249
|
+
|
|
250
|
+
# 实例化并运行导入器
|
|
251
|
+
runner = MockRunner()
|
|
252
|
+
importer = TestImporterThread(None, self.config)
|
|
253
|
+
importer.message = MockSignal(runner.on_message_from_thread)
|
|
254
|
+
importer.run()
|
|
255
|
+
|
|
256
|
+
# 等待导入完成
|
|
257
|
+
start_time = time.time()
|
|
258
|
+
while not runner.finished and time.time() - start_time < 10:
|
|
259
|
+
time.sleep(0.01)
|
|
260
|
+
self.assertTrue(runner.finished, "导入过程未能按预期在10秒内完成, 数据校验无法进行。")
|
|
261
|
+
|
|
262
|
+
# --- 数据校验阶段 ---
|
|
263
|
+
h5_file_path = os.path.join(self.hdf5_dest_dir, 'sh_day.h5')
|
|
264
|
+
self.assertTrue(os.path.exists(h5_file_path), f"目标HDF5文件未被创建于路径: {h5_file_path}")
|
|
265
|
+
|
|
266
|
+
with h5py.File(h5_file_path, 'r') as f:
|
|
267
|
+
# 1. 检查HDF5文件内部结构是否符合预期
|
|
268
|
+
self.assertIn('data', f, "HDF5文件中必须存在顶层组 '/data'")
|
|
269
|
+
data_group = f['/data']
|
|
270
|
+
|
|
271
|
+
# 2. 检查对应股票的数据集是否存在 (注意: 导入时会自动转为大写)
|
|
272
|
+
stock_code = 'SH600000'
|
|
273
|
+
self.assertIn(stock_code, data_group, f"数据集中应存在股票代码为 '{stock_code}' 的表")
|
|
274
|
+
|
|
275
|
+
dset = data_group[stock_code]
|
|
276
|
+
|
|
277
|
+
# 如果记录数为1, 则认为是虚拟数据, 执行严格的逐字段校验
|
|
278
|
+
if dset.shape[0] == 1:
|
|
279
|
+
record = dset[0]
|
|
280
|
+
# 3. 逐字段验证数据是否经过了正确的转换和存储
|
|
281
|
+
expected_datetime = 202301030000
|
|
282
|
+
self.assertEqual(record['datetime'], expected_datetime, f"日期时间戳不匹配. 预期: {expected_datetime}, 实际: {record['datetime']}")
|
|
283
|
+
|
|
284
|
+
# 注意: HDF5中价格为放大1000倍的整数, 成交额单位为千元, 成交量单位为百股
|
|
285
|
+
expected_open = 10.0
|
|
286
|
+
actual_open = record['openPrice'] / 1000.0
|
|
287
|
+
self.assertTrue(np.isclose(actual_open, expected_open), f"开盘价不匹配. 预期: {expected_open}, 实际: {actual_open}")
|
|
288
|
+
|
|
289
|
+
expected_high = 11.5
|
|
290
|
+
actual_high = record['highPrice'] / 1000.0
|
|
291
|
+
self.assertTrue(np.isclose(actual_high, expected_high), f"最高价不匹配. 预期: {expected_high}, 实际: {actual_high}")
|
|
292
|
+
|
|
293
|
+
expected_low = 9.8
|
|
294
|
+
actual_low = record['lowPrice'] / 1000.0
|
|
295
|
+
self.assertTrue(np.isclose(actual_low, expected_low), f"最低价不匹配. 预期: {expected_low}, 实际: {actual_low}")
|
|
296
|
+
|
|
297
|
+
expected_close = 11.2
|
|
298
|
+
actual_close = record['closePrice'] / 1000.0
|
|
299
|
+
self.assertTrue(np.isclose(actual_close, expected_close), f"收盘价不匹配. 预期: {expected_close}, 实际: {actual_close}")
|
|
300
|
+
|
|
301
|
+
expected_amount = 550 # 550000.0 / 1000
|
|
302
|
+
actual_amount = record['transAmount']
|
|
303
|
+
self.assertTrue(np.isclose(actual_amount, expected_amount), f"成交金额不匹配. 预期: {expected_amount} (千元), 实际: {actual_amount}")
|
|
304
|
+
|
|
305
|
+
expected_volume = 500 # 50000 / 100
|
|
306
|
+
actual_volume = record['transCount']
|
|
307
|
+
self.assertTrue(np.isclose(actual_volume, expected_volume), f"成交量不匹配. 预期: {expected_volume} (百股), 实际: {actual_volume}")
|
|
308
|
+
|
|
309
|
+
# 如果记录数大于1, 则认为是真实的通达信数据, 只做基本的存在性检查
|
|
310
|
+
else:
|
|
311
|
+
self.assertTrue(dset.shape[0] > 1, f"数据记录数量不正确. 预期: > 1, 实际: {dset.shape[0]}")
|
|
312
|
+
|
|
313
|
+
if __name__ == '__main__':
|
|
314
|
+
# 允许此文件作为独立的脚本直接运行, 方便单独调试
|
|
315
|
+
unittest.main()
|