hikyuu 2.6.6__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 +10 -0
- hikyuu/__init__.pyi +567 -560
- hikyuu/analysis/__init__.pyi +539 -519
- hikyuu/analysis/analysis.pyi +540 -520
- hikyuu/core.pyi +541 -521
- hikyuu/cpp/__init__.pyi +2 -2
- hikyuu/cpp/core310.pyd +0 -0
- hikyuu/cpp/core310.pyi +1041 -859
- hikyuu/cpp/core311.pyd +0 -0
- hikyuu/cpp/core311.pyi +1041 -859
- hikyuu/cpp/core312.pyd +0 -0
- hikyuu/cpp/core312.pyi +1041 -859
- hikyuu/cpp/core313.pyd +0 -0
- hikyuu/cpp/core313.pyi +1041 -857
- hikyuu/cpp/core39.pyd +0 -0
- hikyuu/cpp/core39.pyi +1041 -859
- hikyuu/cpp/hikyuu.dll +0 -0
- hikyuu/cpp/hikyuu.lib +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/common_clickhouse.py +0 -47
- hikyuu/data/tdx_to_h5.py +1 -1
- hikyuu/draw/__init__.pyi +1 -1
- hikyuu/draw/drawplot/__init__.pyi +9 -9
- hikyuu/draw/drawplot/bokeh_draw.pyi +556 -551
- hikyuu/draw/drawplot/common.pyi +1 -1
- hikyuu/draw/drawplot/echarts_draw.py +9 -8
- hikyuu/draw/drawplot/echarts_draw.pyi +558 -553
- hikyuu/draw/drawplot/matplotlib_draw.py +3 -3
- hikyuu/draw/drawplot/matplotlib_draw.pyi +568 -563
- hikyuu/draw/elder.pyi +11 -11
- hikyuu/draw/kaufman.py +1 -1
- hikyuu/draw/kaufman.pyi +18 -18
- hikyuu/draw/volume.pyi +10 -10
- 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 +551 -567
- hikyuu/gui/HikyuuTDX.py +2 -4
- hikyuu/gui/data/MainWindow.py +185 -174
- hikyuu/hub.pyi +6 -6
- hikyuu/include/hikyuu/DataType.h +1 -10
- 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 +4 -3
- hikyuu/include/hikyuu/StockTypeInfo.h +6 -0
- hikyuu/include/hikyuu/TransRecord.h +2 -8
- hikyuu/include/hikyuu/doc.h +4 -0
- hikyuu/include/hikyuu/indicator/Indicator.h +37 -0
- hikyuu/include/hikyuu/lang.h +27 -0
- hikyuu/include/hikyuu/plugin/KDataToHdf5Importer.h +6 -1
- hikyuu/include/hikyuu/plugin/hkuextra.h +56 -0
- hikyuu/include/hikyuu/plugin/interface/HkuExtraPluginInterface.h +38 -0
- hikyuu/include/hikyuu/plugin/interface/ImportKDataToHdf5PluginInterface.h +10 -1
- hikyuu/include/hikyuu/plugin/interface/plugins.h +2 -0
- hikyuu/include/hikyuu/python/pybind_utils.h +9 -0
- hikyuu/include/hikyuu/trade_manage/TradeRecord.h +1 -1
- hikyuu/include/hikyuu/utilities/config.h +0 -2
- hikyuu/include/hikyuu/utilities/os.h +3 -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 +556 -551
- hikyuu/trade_manage/broker.pyi +3 -3
- hikyuu/trade_manage/broker_easytrader.pyi +1 -1
- hikyuu/trade_manage/trade.py +4 -65
- hikyuu/trade_manage/trade.pyi +556 -563
- hikyuu/trade_sys/__init__.py +11 -0
- hikyuu/util/__init__.pyi +0 -1
- hikyuu/util/singleton.pyi +1 -1
- {hikyuu-2.6.6.dist-info → hikyuu-2.6.7.dist-info}/METADATA +6 -4
- {hikyuu-2.6.6.dist-info → hikyuu-2.6.7.dist-info}/RECORD +102 -95
- {hikyuu-2.6.6.dist-info → hikyuu-2.6.7.dist-info}/top_level.txt +2 -1
- hikyuu/cpp/i18n/zh_CN.mo +0 -0
- hikyuu/include/hikyuu/utilities/mo/mo.h +0 -64
- hikyuu/indicator/talib_wrap.py +0 -1273
- /hikyuu/include/hikyuu/utilities/{mo/moFileReader.h → moFileReader.h} +0 -0
- /hikyuu/include/hikyuu/{utilities/mo → view}/__init__.py +0 -0
- {hikyuu-2.6.6.dist-info → hikyuu-2.6.7.dist-info}/LICENSE +0 -0
- {hikyuu-2.6.6.dist-info → hikyuu-2.6.7.dist-info}/WHEEL +0 -0
- {hikyuu-2.6.6.dist-info → hikyuu-2.6.7.dist-info}/entry_points.txt +0 -0
hikyuu/indicator/indicator.py
CHANGED
|
@@ -29,50 +29,9 @@ from hikyuu import Datetime
|
|
|
29
29
|
import pandas as pd
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def indicator_iter(indicator):
|
|
33
|
-
for i in range(len(indicator)):
|
|
34
|
-
yield indicator[i]
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def indicator_getitem(data, i):
|
|
38
|
-
"""
|
|
39
|
-
:param i: int | Datetime | slice | str 类型
|
|
40
|
-
"""
|
|
41
|
-
if isinstance(i, int):
|
|
42
|
-
length = len(data)
|
|
43
|
-
index = length + i if i < 0 else i
|
|
44
|
-
if index < 0 or index >= length:
|
|
45
|
-
raise IndexError("index out of range: %d" % i)
|
|
46
|
-
return data.get(index)
|
|
47
|
-
|
|
48
|
-
elif isinstance(i, slice):
|
|
49
|
-
return [data.get(x) for x in range(*i.indices(len(data)))]
|
|
50
|
-
|
|
51
|
-
elif isinstance(i, Datetime):
|
|
52
|
-
return data.get_by_datetime(i)
|
|
53
|
-
|
|
54
|
-
elif isinstance(i, str):
|
|
55
|
-
return data.get_by_datetime(Datetime(i))
|
|
56
|
-
|
|
57
|
-
else:
|
|
58
|
-
raise IndexError("Error index type")
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Indicator.__getitem__ = indicator_getitem
|
|
62
|
-
Indicator.__iter__ = indicator_iter
|
|
63
|
-
|
|
64
|
-
|
|
65
32
|
def indicator_to_df(indicator):
|
|
66
33
|
"""转化为pandas.DataFrame"""
|
|
67
|
-
|
|
68
|
-
return pd.DataFrame(indicator.to_np(), columns=[indicator.name])
|
|
69
|
-
data = {}
|
|
70
|
-
name = indicator.name
|
|
71
|
-
columns = []
|
|
72
|
-
for i in range(indicator.get_result_num()):
|
|
73
|
-
data[name + str(i)] = indicator.get_result(i)
|
|
74
|
-
columns.append(name + str(i + 1))
|
|
75
|
-
return pd.DataFrame(data, columns=columns)
|
|
34
|
+
return pd.DataFrame.from_records(indicator.to_np())
|
|
76
35
|
|
|
77
36
|
|
|
78
37
|
Indicator.to_df = indicator_to_df
|
|
@@ -107,17 +66,19 @@ def concat_to_df(dates, ind_list, head_stock_code=True, head_ind_name=False):
|
|
|
107
66
|
198 2024-03-06 00:00:00 10.070455 9.776818
|
|
108
67
|
199 2024-03-07 00:00:00 10.101364 9.738182
|
|
109
68
|
"""
|
|
110
|
-
df =
|
|
111
|
-
|
|
112
|
-
|
|
69
|
+
df = dates.to_df('ms')
|
|
70
|
+
if not ind_list:
|
|
71
|
+
return df
|
|
72
|
+
for i in range(len(ind_list)):
|
|
73
|
+
ind = ind_list[i]
|
|
113
74
|
if head_ind_name and head_stock_code:
|
|
114
|
-
|
|
75
|
+
name = f"{ind.name}/{ind.get_context().get_stock().market_code}"
|
|
115
76
|
elif head_ind_name:
|
|
116
|
-
|
|
77
|
+
name = f'{ind.name}{i}'
|
|
117
78
|
else:
|
|
118
|
-
|
|
119
|
-
df = pd.
|
|
120
|
-
|
|
79
|
+
name = ind.get_context().get_stock().market_code
|
|
80
|
+
df = pd.merge(df, ind.to_df()[['datetime', 'value1']].rename(
|
|
81
|
+
columns={'value1': name}), on='datetime', how='left')
|
|
121
82
|
return df
|
|
122
83
|
|
|
123
84
|
|
|
@@ -137,10 +98,10 @@ def df_to_ind(df, col_name, col_date=None):
|
|
|
137
98
|
:return: Indicator
|
|
138
99
|
"""
|
|
139
100
|
if col_date is not None:
|
|
140
|
-
dates = df[col_date]
|
|
101
|
+
dates = df[col_date]
|
|
141
102
|
dates = DatetimeList([Datetime(x) for x in dates])
|
|
142
|
-
return PRICELIST(df[col_name]
|
|
143
|
-
return PRICELIST(df[col_name]
|
|
103
|
+
return PRICELIST(df[col_name], align_dates=dates)
|
|
104
|
+
return PRICELIST(df[col_name])
|
|
144
105
|
|
|
145
106
|
|
|
146
107
|
# 避免 python 中公式原型必须加括号
|
hikyuu/plugin/backtest.dll
CHANGED
|
Binary file
|
|
Binary file
|
hikyuu/plugin/dataserver.dll
CHANGED
|
Binary file
|
hikyuu/plugin/device.dll
CHANGED
|
Binary file
|
hikyuu/plugin/extind.dll
CHANGED
|
Binary file
|
|
Binary file
|
hikyuu/plugin/import2hdf5.dll
CHANGED
|
Binary file
|
hikyuu/plugin/tmreport.dll
CHANGED
|
Binary file
|
hikyuu/test/test_init.py
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
# 历史:1)20130128, Added by fasiondog
|
|
8
8
|
#===============================================================================
|
|
9
9
|
from hikyuu import *
|
|
10
|
+
from configparser import ConfigParser
|
|
11
|
+
from functools import lru_cache
|
|
10
12
|
|
|
11
13
|
import os
|
|
12
14
|
#curdir = os.path.dirname(os.path.realpath(__file__))
|
|
@@ -34,3 +36,60 @@ hikyuu_init(config_file)
|
|
|
34
36
|
sm = StockManager.instance()
|
|
35
37
|
#endtime = time.time()
|
|
36
38
|
#print "%.2fs" % (endtime-starttime)
|
|
39
|
+
|
|
40
|
+
# 仅在模块加载时读取一次配置
|
|
41
|
+
_config = ConfigParser()
|
|
42
|
+
_config.read(config_file)
|
|
43
|
+
_tdx_dir = _config.get('tdx', 'dir', fallback='/mnt/tdx')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@lru_cache(maxsize=None)
|
|
47
|
+
def get_real_tdx_filepath(code, market='sh'):
|
|
48
|
+
"""
|
|
49
|
+
获取真实的通达信.day文件路径。
|
|
50
|
+
|
|
51
|
+
该函数按以下顺序确定文件路径:
|
|
52
|
+
1. 检查环境变量 `HKU_real_tdx_file_path` 是否被设置。
|
|
53
|
+
2. 如果未设置, 则检查配置文件中 "tdx"->"dir" 指定的路径。
|
|
54
|
+
3. 如果未配置, 则使用 /mnt/tdx/vipdoc/。
|
|
55
|
+
4. 如果上述路径不存在, 则检查 test_data/vipdoc/ 是否存在, 如果存在, 则使用该路径。
|
|
56
|
+
5. 如果上述路径都不存在, 则自动回退到在项目根目录下的 `test_data` 文件夹中寻找同名文件作为备用。
|
|
57
|
+
|
|
58
|
+
这使得测试既能适应有权访问生产数据环境的开发者, 也能让其他贡献者使用
|
|
59
|
+
项目内提供的样本数据来运行测试, 避免了因路径问题导致的测试挂起或失败。
|
|
60
|
+
|
|
61
|
+
:param str code: 股票代码 (如 '600000', '000001')
|
|
62
|
+
:param str market: 市场简称 (如 'sh', 'sz')
|
|
63
|
+
:return: 最终确定的文件路径 (可能不存在, 由调用方处理)
|
|
64
|
+
"""
|
|
65
|
+
# 检查环境变量
|
|
66
|
+
env_path = os.environ.get('HKU_real_tdx_file_path')
|
|
67
|
+
if env_path and os.path.exists(env_path):
|
|
68
|
+
print(f"get_real_tdx_filepath return: {env_path}")
|
|
69
|
+
return env_path
|
|
70
|
+
|
|
71
|
+
market_subdir = f"{market.lower()}/lday"
|
|
72
|
+
filename = f"{market.lower()}{code}.day"
|
|
73
|
+
|
|
74
|
+
# 检查配置文件指定的路径
|
|
75
|
+
base_path = os.path.join(_tdx_dir, 'vipdoc')
|
|
76
|
+
if os.path.exists(base_path):
|
|
77
|
+
primary_path = os.path.join(base_path, market_subdir, filename)
|
|
78
|
+
if os.path.exists(primary_path):
|
|
79
|
+
print(f"get_real_tdx_filepath return: {primary_path}")
|
|
80
|
+
return primary_path
|
|
81
|
+
|
|
82
|
+
# 检查 test_data/vipdoc/
|
|
83
|
+
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
84
|
+
test_data_vipdoc_path = os.path.join(project_root, 'test_data', 'vipdoc')
|
|
85
|
+
if os.path.exists(test_data_vipdoc_path):
|
|
86
|
+
primary_path = os.path.join(test_data_vipdoc_path, market_subdir, filename)
|
|
87
|
+
if os.path.exists(primary_path):
|
|
88
|
+
print(f"get_real_tdx_filepath return: {primary_path}")
|
|
89
|
+
return primary_path
|
|
90
|
+
|
|
91
|
+
# 如果主路径文件不存在, 则尝试从 test_data 目录寻找同名文件作为备用
|
|
92
|
+
test_data_dir = os.path.join(project_root, 'test_data')
|
|
93
|
+
fallback_path = os.path.join(test_data_dir, filename)
|
|
94
|
+
print(f"get_real_tdx_filepath return: {fallback_path}")
|
|
95
|
+
return fallback_path
|
|
@@ -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("[验证成功] 断言已在正确的位置因正确的原因失败。")
|