hqdata 0.1.2__tar.gz
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.
- hqdata-0.1.2/LICENSE +21 -0
- hqdata-0.1.2/PKG-INFO +106 -0
- hqdata-0.1.2/README.md +88 -0
- hqdata-0.1.2/hqdata/__init__.py +6 -0
- hqdata-0.1.2/hqdata/api.py +91 -0
- hqdata-0.1.2/hqdata/config.py +14 -0
- hqdata-0.1.2/hqdata/sources/__init__.py +1 -0
- hqdata-0.1.2/hqdata/sources/base.py +70 -0
- hqdata-0.1.2/hqdata/sources/ricequant.py +74 -0
- hqdata-0.1.2/hqdata/sources/tushare.py +113 -0
- hqdata-0.1.2/hqdata.egg-info/PKG-INFO +106 -0
- hqdata-0.1.2/hqdata.egg-info/SOURCES.txt +17 -0
- hqdata-0.1.2/hqdata.egg-info/dependency_links.txt +1 -0
- hqdata-0.1.2/hqdata.egg-info/requires.txt +12 -0
- hqdata-0.1.2/hqdata.egg-info/top_level.txt +1 -0
- hqdata-0.1.2/pyproject.toml +33 -0
- hqdata-0.1.2/setup.cfg +4 -0
- hqdata-0.1.2/tests/test_ricequant.py +34 -0
- hqdata-0.1.2/tests/test_tushare.py +78 -0
hqdata-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HonestQuant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
hqdata-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hqdata
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A股历史与实时行情数据统一接入、清洗与存储
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pandas>=2.3.3
|
|
9
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
10
|
+
Provides-Extra: tushare
|
|
11
|
+
Requires-Dist: tushare>=1.4.29; extra == "tushare"
|
|
12
|
+
Provides-Extra: ricequant
|
|
13
|
+
Requires-Dist: rqdatac>=3.1.4; extra == "ricequant"
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# hqdata - A股历史与实时行情数据统一接入、清洗与存储
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img src ="https://img.shields.io/pypi/v/hqdata.svg"/>
|
|
23
|
+
<img src ="https://img.shields.io/pypi/pyversions/hqdata.svg"/>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
## 定位
|
|
27
|
+
|
|
28
|
+
`hqdata` 是 HonestQuant 量化系统的**数据基础层**,职责边界清晰:
|
|
29
|
+
|
|
30
|
+
- 对下:封装各数据源 SDK,屏蔽接口差异
|
|
31
|
+
- 对上:提供统一的查询接口
|
|
32
|
+
- 上层策略和引擎**只调用 `hqdata.api`**,不直接接触任何数据源
|
|
33
|
+
|
|
34
|
+
## 支持的数据源
|
|
35
|
+
|
|
36
|
+
| 数据源 | 状态 | 说明 |
|
|
37
|
+
| ----------- | ------ | ------------------------------------------- |
|
|
38
|
+
| **AKShare** | 计划中 | 免费,实时数据 |
|
|
39
|
+
| **Tushare** | 已接入 | 需满足账户积分要求,支持股票&指数的历史日线 |
|
|
40
|
+
| **米筐** | 接入中 | 需license,支持Tick |
|
|
41
|
+
| **迅投** | 计划中 | 需迅投终端 |
|
|
42
|
+
| **iTick** | 计划中 | 需注册 |
|
|
43
|
+
|
|
44
|
+
## 安装
|
|
45
|
+
|
|
46
|
+
### 方式一:通过pip安装
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# 基础安装(仅包含核心功能)
|
|
50
|
+
pip install hqdata
|
|
51
|
+
|
|
52
|
+
# 按需安装数据源依赖
|
|
53
|
+
pip install hqdata[tushare] # Tushare 支持
|
|
54
|
+
pip install hqdata[ricequant] # 米筐支持
|
|
55
|
+
pip install hqdata[tushare,ricequant] # 同时安装两者
|
|
56
|
+
|
|
57
|
+
# 复制模板并自行填入所需字段
|
|
58
|
+
cp .env.example .env # 放在你运行 Python 代码的当前目录(优先)/包安装目录
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 方式二:本地开发
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# 创建虚拟环境
|
|
65
|
+
python -m venv .venv
|
|
66
|
+
source .venv/bin/activate
|
|
67
|
+
|
|
68
|
+
# 安装依赖 (editable 模式,改代码直接生效)
|
|
69
|
+
pip install -e .
|
|
70
|
+
|
|
71
|
+
# 复制模板并自行填入所需字段
|
|
72
|
+
cp .env.example .env # 放在你运行 Python 代码的当前目录(优先)/包安装目录
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 使用
|
|
76
|
+
|
|
77
|
+
以Tushare数据为例
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from hqdata import init_source, get_bar
|
|
81
|
+
|
|
82
|
+
# 初始化 Tushare (日线数据)
|
|
83
|
+
init_source("tushare")
|
|
84
|
+
|
|
85
|
+
# 获取日线数据 (symbol 格式: "代码.交易所")
|
|
86
|
+
df = get_bar("600000.SH", frequency="1day", start_date="20260401", end_date="20260402")
|
|
87
|
+
print(df.head())
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 输入参数格式说明
|
|
91
|
+
|
|
92
|
+
### 股票代码
|
|
93
|
+
|
|
94
|
+
symbol 参数统一使用 `代码.交易所` 格式:
|
|
95
|
+
|
|
96
|
+
| 交易所 | 代码 | 示例 |
|
|
97
|
+
| ------ | ---- | ----------- |
|
|
98
|
+
| 上交所 | SH | `600000.SH` |
|
|
99
|
+
| 深交所 | SZ | `000001.SZ` |
|
|
100
|
+
|
|
101
|
+
## 测试
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pytest tests/ -v
|
|
105
|
+
pytest tests/test_tushare.py::TestTushareIntegration::test_get_bar # 运行单个测试
|
|
106
|
+
```
|
hqdata-0.1.2/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# hqdata - A股历史与实时行情数据统一接入、清洗与存储
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src ="https://img.shields.io/pypi/v/hqdata.svg"/>
|
|
5
|
+
<img src ="https://img.shields.io/pypi/pyversions/hqdata.svg"/>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
## 定位
|
|
9
|
+
|
|
10
|
+
`hqdata` 是 HonestQuant 量化系统的**数据基础层**,职责边界清晰:
|
|
11
|
+
|
|
12
|
+
- 对下:封装各数据源 SDK,屏蔽接口差异
|
|
13
|
+
- 对上:提供统一的查询接口
|
|
14
|
+
- 上层策略和引擎**只调用 `hqdata.api`**,不直接接触任何数据源
|
|
15
|
+
|
|
16
|
+
## 支持的数据源
|
|
17
|
+
|
|
18
|
+
| 数据源 | 状态 | 说明 |
|
|
19
|
+
| ----------- | ------ | ------------------------------------------- |
|
|
20
|
+
| **AKShare** | 计划中 | 免费,实时数据 |
|
|
21
|
+
| **Tushare** | 已接入 | 需满足账户积分要求,支持股票&指数的历史日线 |
|
|
22
|
+
| **米筐** | 接入中 | 需license,支持Tick |
|
|
23
|
+
| **迅投** | 计划中 | 需迅投终端 |
|
|
24
|
+
| **iTick** | 计划中 | 需注册 |
|
|
25
|
+
|
|
26
|
+
## 安装
|
|
27
|
+
|
|
28
|
+
### 方式一:通过pip安装
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 基础安装(仅包含核心功能)
|
|
32
|
+
pip install hqdata
|
|
33
|
+
|
|
34
|
+
# 按需安装数据源依赖
|
|
35
|
+
pip install hqdata[tushare] # Tushare 支持
|
|
36
|
+
pip install hqdata[ricequant] # 米筐支持
|
|
37
|
+
pip install hqdata[tushare,ricequant] # 同时安装两者
|
|
38
|
+
|
|
39
|
+
# 复制模板并自行填入所需字段
|
|
40
|
+
cp .env.example .env # 放在你运行 Python 代码的当前目录(优先)/包安装目录
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 方式二:本地开发
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# 创建虚拟环境
|
|
47
|
+
python -m venv .venv
|
|
48
|
+
source .venv/bin/activate
|
|
49
|
+
|
|
50
|
+
# 安装依赖 (editable 模式,改代码直接生效)
|
|
51
|
+
pip install -e .
|
|
52
|
+
|
|
53
|
+
# 复制模板并自行填入所需字段
|
|
54
|
+
cp .env.example .env # 放在你运行 Python 代码的当前目录(优先)/包安装目录
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 使用
|
|
58
|
+
|
|
59
|
+
以Tushare数据为例
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from hqdata import init_source, get_bar
|
|
63
|
+
|
|
64
|
+
# 初始化 Tushare (日线数据)
|
|
65
|
+
init_source("tushare")
|
|
66
|
+
|
|
67
|
+
# 获取日线数据 (symbol 格式: "代码.交易所")
|
|
68
|
+
df = get_bar("600000.SH", frequency="1day", start_date="20260401", end_date="20260402")
|
|
69
|
+
print(df.head())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 输入参数格式说明
|
|
73
|
+
|
|
74
|
+
### 股票代码
|
|
75
|
+
|
|
76
|
+
symbol 参数统一使用 `代码.交易所` 格式:
|
|
77
|
+
|
|
78
|
+
| 交易所 | 代码 | 示例 |
|
|
79
|
+
| ------ | ---- | ----------- |
|
|
80
|
+
| 上交所 | SH | `600000.SH` |
|
|
81
|
+
| 深交所 | SZ | `000001.SZ` |
|
|
82
|
+
|
|
83
|
+
## 测试
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pytest tests/ -v
|
|
87
|
+
pytest tests/test_tushare.py::TestTushareIntegration::test_get_bar # 运行单个测试
|
|
88
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""hqdata public API - only entry point for upper layers"""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from typing import Literal, Optional
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
# Singleton source instance
|
|
8
|
+
_source: Optional["BaseSource"] = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def init_source(source_type: Literal["ricequant", "tushare"], **kwargs) -> None:
|
|
12
|
+
"""Initialize data source.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
source_type: "ricequant" or "tushare"
|
|
16
|
+
**kwargs: Source-specific credentials
|
|
17
|
+
"""
|
|
18
|
+
global _source
|
|
19
|
+
if source_type == "ricequant":
|
|
20
|
+
from hqdata.sources.ricequant import RicequantSource
|
|
21
|
+
|
|
22
|
+
_source = RicequantSource(**kwargs)
|
|
23
|
+
elif source_type == "tushare":
|
|
24
|
+
from hqdata.sources.tushare import TushareSource
|
|
25
|
+
|
|
26
|
+
_source = TushareSource(**kwargs)
|
|
27
|
+
else:
|
|
28
|
+
raise ValueError(f"Unknown source type: {source_type}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_stock_list(list_status: str = "L") -> pd.DataFrame:
|
|
32
|
+
"""Get basic info for stocks.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
list_status: Listing status — "L" (listed), "D" (delisted), "P" (suspended)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
DataFrame with columns: symbol, name, industry, market, exchange,
|
|
39
|
+
curr_type, list_status, list_date, delist_date, is_hs
|
|
40
|
+
"""
|
|
41
|
+
if _source is None:
|
|
42
|
+
raise RuntimeError("Data source not initialized. Call init_source() first.")
|
|
43
|
+
return _source.get_stock_list(list_status)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_bar(
|
|
47
|
+
symbol: str,
|
|
48
|
+
frequency: str = "1day",
|
|
49
|
+
start_date: Optional[str] = None,
|
|
50
|
+
end_date: Optional[str] = None,
|
|
51
|
+
) -> pd.DataFrame:
|
|
52
|
+
"""Get K-line/bar data for a stock.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
symbol: Stock symbol with exchange (e.g., "600000.SH" or "000001.SZ")
|
|
56
|
+
frequency: Bar frequency ("1day" | "1min" | "5min" | "15min" | "30min" | "60min" | "1week" | "1month")
|
|
57
|
+
start_date: Start date in YYYYMMDD format (defaults to today)
|
|
58
|
+
end_date: End date in YYYYMMDD format (defaults to today)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
DataFrame with columns: symbol, date, open, high, low, close, pre_close, change, pct_change, volume, amount
|
|
62
|
+
"""
|
|
63
|
+
if _source is None:
|
|
64
|
+
raise RuntimeError("Data source not initialized. Call init_source() first.")
|
|
65
|
+
today = date.today().strftime("%Y%m%d")
|
|
66
|
+
start_date = start_date or today
|
|
67
|
+
end_date = end_date or today
|
|
68
|
+
return _source.get_bar(symbol, frequency, start_date, end_date)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_index_bar(
|
|
72
|
+
symbol: str,
|
|
73
|
+
start_date: Optional[str] = None,
|
|
74
|
+
end_date: Optional[str] = None,
|
|
75
|
+
) -> pd.DataFrame:
|
|
76
|
+
"""Get daily bar data for an index.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
symbol: Index code with exchange (e.g., "000001.SH", "399300.SZ")
|
|
80
|
+
start_date: Start date in YYYYMMDD format (defaults to today)
|
|
81
|
+
end_date: End date in YYYYMMDD format (defaults to today)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
DataFrame with columns: symbol, date, open, high, low, close, pre_close, change, pct_change, volume, amount
|
|
85
|
+
"""
|
|
86
|
+
if _source is None:
|
|
87
|
+
raise RuntimeError("Data source not initialized. Call init_source() first.")
|
|
88
|
+
today = date.today().strftime("%Y%m%d")
|
|
89
|
+
start_date = start_date or today
|
|
90
|
+
end_date = end_date or today
|
|
91
|
+
return _source.get_index_bar(symbol, start_date, end_date)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Configuration loading from .env file"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
# Load .env from current working directory first (user's project root)
|
|
7
|
+
# Then fall back to package directory (for development)
|
|
8
|
+
_cwd_env = Path.cwd() / ".env"
|
|
9
|
+
_pkg_env = Path(__file__).parent.parent / ".env"
|
|
10
|
+
|
|
11
|
+
if _cwd_env.exists():
|
|
12
|
+
load_dotenv(_cwd_env)
|
|
13
|
+
elif _pkg_env.exists():
|
|
14
|
+
load_dotenv(_pkg_env)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""hqdata data sources"""
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Base class for data sources"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Optional
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseSource(ABC):
|
|
10
|
+
"""Abstract base class for data source adapters."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def _get_env(param: Optional[str], env_var: str, error_msg: str) -> str:
|
|
14
|
+
value = param or os.getenv(env_var)
|
|
15
|
+
if not value:
|
|
16
|
+
raise ValueError(error_msg)
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def get_bar(
|
|
21
|
+
self,
|
|
22
|
+
symbol: str,
|
|
23
|
+
frequency: str = "1day",
|
|
24
|
+
start_date: Optional[str] = None,
|
|
25
|
+
end_date: Optional[str] = None,
|
|
26
|
+
) -> pd.DataFrame:
|
|
27
|
+
"""Get K-line/bar data for a stock.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
symbol: Stock symbol with exchange (e.g., "600000.SH" or "000001.SZ")
|
|
31
|
+
frequency: Bar frequency ("1min", "5min", "15min", "30min", "60min", "1day", "1week", "1month")
|
|
32
|
+
start_date: Start date in YYYYMMDD format
|
|
33
|
+
end_date: End date in YYYYMMDD format
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
DataFrame with columns: symbol, date, open, high, low, close, pre_close, change, pct_change, volume, amount
|
|
37
|
+
"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def get_stock_list(self, list_status: str = "L") -> pd.DataFrame:
|
|
42
|
+
"""Get basic info for stocks.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
list_status: Listing status — "L" (listed), "D" (delisted), "P" (suspended)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
DataFrame with columns: symbol, name, industry, market, exchange,
|
|
49
|
+
curr_type, list_status, list_date, delist_date, is_hs
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def get_index_bar(
|
|
55
|
+
self,
|
|
56
|
+
symbol: str,
|
|
57
|
+
start_date: Optional[str] = None,
|
|
58
|
+
end_date: Optional[str] = None,
|
|
59
|
+
) -> pd.DataFrame:
|
|
60
|
+
"""Get daily bar data for an index.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
symbol: Index code with exchange (e.g., "000300.SH", "000905.SH")
|
|
64
|
+
start_date: Start date in YYYYMMDD format
|
|
65
|
+
end_date: End date in YYYYMMDD format
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
DataFrame with columns: symbol, date, open, high, low, close, pre_close, change, pct_change, volume, amount
|
|
69
|
+
"""
|
|
70
|
+
pass
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Ricequant (米筐) data source adapter"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from hqdata.sources.base import BaseSource
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_rqdatac():
|
|
10
|
+
"""Lazy import rqdatac to support optional installation."""
|
|
11
|
+
from rqdatac import init, get_price
|
|
12
|
+
from rqdatac.share.errors import RQDataError
|
|
13
|
+
return init, get_price, RQDataError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RicequantSource(BaseSource):
|
|
17
|
+
"""Ricequant data source adapter.
|
|
18
|
+
|
|
19
|
+
Requires rqdatac >= 3.1.4 and valid RQData credentials.
|
|
20
|
+
Credentials can be provided via arguments or environment variables
|
|
21
|
+
(RQDATA_USERNAME, RQDATA_PASSWORD).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
username: Optional[str] = None,
|
|
27
|
+
password: Optional[str] = None,
|
|
28
|
+
):
|
|
29
|
+
"""Initialize ricequant connection.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
username: RQData username (email), defaults to RQDATA_USERNAME env var
|
|
33
|
+
password: RQData password/token, defaults to RQDATA_PASSWORD env var
|
|
34
|
+
"""
|
|
35
|
+
username = BaseSource._get_env(
|
|
36
|
+
username, "RQDATA_USERNAME",
|
|
37
|
+
"RQData credentials not provided. Set username/password in init_source() "
|
|
38
|
+
"or set RQDATA_USERNAME and RQDATA_PASSWORD environment variables."
|
|
39
|
+
)
|
|
40
|
+
password = BaseSource._get_env(
|
|
41
|
+
password, "RQDATA_PASSWORD",
|
|
42
|
+
"RQData credentials not provided. Set username/password in init_source() "
|
|
43
|
+
"or set RQDATA_USERNAME and RQDATA_PASSWORD environment variables."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
init, _, _ = _get_rqdatac()
|
|
47
|
+
init(
|
|
48
|
+
username,
|
|
49
|
+
password,
|
|
50
|
+
address=("rqdatad-pro.ricequant.com", 16011),
|
|
51
|
+
use_pool=True,
|
|
52
|
+
max_pool_size=1,
|
|
53
|
+
auto_load_plugins=False,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def get_stock_list(self, list_status: str = "L") -> pd.DataFrame:
|
|
57
|
+
raise NotImplementedError("Ricequant get_stock_list not implemented yet.")
|
|
58
|
+
|
|
59
|
+
def get_bar(
|
|
60
|
+
self,
|
|
61
|
+
symbol: str,
|
|
62
|
+
frequency: str = "1day",
|
|
63
|
+
start_date: Optional[str] = None,
|
|
64
|
+
end_date: Optional[str] = None,
|
|
65
|
+
) -> pd.DataFrame:
|
|
66
|
+
raise NotImplementedError("Ricequant get_bar not implemented yet.")
|
|
67
|
+
|
|
68
|
+
def get_index_bar(
|
|
69
|
+
self,
|
|
70
|
+
symbol: str,
|
|
71
|
+
start_date: Optional[str] = None,
|
|
72
|
+
end_date: Optional[str] = None,
|
|
73
|
+
) -> pd.DataFrame:
|
|
74
|
+
raise NotImplementedError("Ricequant get_index_bar not implemented yet.")
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Tushare data source adapter"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from hqdata.sources.base import BaseSource
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_tushare():
|
|
10
|
+
"""Lazy import tushare to support optional installation."""
|
|
11
|
+
import tushare as ts
|
|
12
|
+
return ts
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TushareSource(BaseSource):
|
|
16
|
+
"""Tushare data source adapter.
|
|
17
|
+
|
|
18
|
+
Requires tushare >= 1.4.0 and valid TUSHARE_TOKEN.
|
|
19
|
+
Token can be set via environment variable TUSHARE_TOKEN.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, token: Optional[str] = None):
|
|
23
|
+
"""Initialize tushare connection.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
token: Tushare token, defaults to TUSHARE_TOKEN env var
|
|
27
|
+
"""
|
|
28
|
+
token = BaseSource._get_env(
|
|
29
|
+
token, "TUSHARE_TOKEN",
|
|
30
|
+
"Tushare token not provided. Set token in init_source() "
|
|
31
|
+
"or set TUSHARE_TOKEN environment variable."
|
|
32
|
+
)
|
|
33
|
+
ts = _get_tushare()
|
|
34
|
+
ts.set_token(token)
|
|
35
|
+
self.pro = ts.pro_api()
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _rename_daily_columns(df: pd.DataFrame) -> pd.DataFrame:
|
|
39
|
+
return df.rename(columns={
|
|
40
|
+
"ts_code": "symbol",
|
|
41
|
+
"trade_date": "date",
|
|
42
|
+
"vol": "volume",
|
|
43
|
+
"pct_chg": "pct_change",
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
_STOCK_LIST_FIELDS = (
|
|
47
|
+
"symbol,name,industry,market,exchange,curr_type,list_status,list_date,delist_date,is_hs"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def get_stock_list(self, list_status: str = "L") -> pd.DataFrame:
|
|
51
|
+
"""Get basic info for stocks.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
list_status: Listing status — "L" (listed), "D" (delisted), "P" (suspended)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
DataFrame with columns: symbol, name, industry, market, exchange,
|
|
58
|
+
curr_type, list_status, list_date, delist_date, is_hs
|
|
59
|
+
"""
|
|
60
|
+
return self.pro.stock_basic(list_status=list_status, fields=self._STOCK_LIST_FIELDS)
|
|
61
|
+
|
|
62
|
+
def get_bar(
|
|
63
|
+
self,
|
|
64
|
+
symbol: str,
|
|
65
|
+
frequency: str = "1day",
|
|
66
|
+
start_date: Optional[str] = None,
|
|
67
|
+
end_date: Optional[str] = None,
|
|
68
|
+
) -> pd.DataFrame:
|
|
69
|
+
"""Get daily bar data for a stock.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
symbol: Stock symbol with exchange (e.g., "600000.SH" or "000001.SZ")
|
|
73
|
+
frequency: Bar frequency ("1day" only, other frequencies not supported)
|
|
74
|
+
start_date: Start date in YYYYMMDD format
|
|
75
|
+
end_date: End date in YYYYMMDD format
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
DataFrame with columns: symbol, date, open, high, low, close, pre_close, change, pct_change, volume, amount
|
|
79
|
+
"""
|
|
80
|
+
if frequency != "1day":
|
|
81
|
+
raise NotImplementedError(
|
|
82
|
+
f"Tushare only supports '1day' frequency, got '{frequency}'"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
df = self.pro.daily(ts_code=symbol, start_date=start_date, end_date=end_date)
|
|
86
|
+
|
|
87
|
+
if df is not None and not df.empty:
|
|
88
|
+
df = self._rename_daily_columns(df).sort_values("date")
|
|
89
|
+
|
|
90
|
+
return df
|
|
91
|
+
|
|
92
|
+
def get_index_bar(
|
|
93
|
+
self,
|
|
94
|
+
symbol: str,
|
|
95
|
+
start_date: Optional[str] = None,
|
|
96
|
+
end_date: Optional[str] = None,
|
|
97
|
+
) -> pd.DataFrame:
|
|
98
|
+
"""Get daily bar data for an index.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
symbol: Index code with exchange (e.g., "000300.SH", "000905.SH")
|
|
102
|
+
start_date: Start date in YYYYMMDD format
|
|
103
|
+
end_date: End date in YYYYMMDD format
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
DataFrame with columns: symbol, date, open, high, low, close, pre_close, change, pct_change, volume, amount
|
|
107
|
+
"""
|
|
108
|
+
df = self.pro.index_daily(ts_code=symbol, start_date=start_date, end_date=end_date)
|
|
109
|
+
|
|
110
|
+
if df is not None and not df.empty:
|
|
111
|
+
df = self._rename_daily_columns(df).sort_values("date")
|
|
112
|
+
|
|
113
|
+
return df
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hqdata
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A股历史与实时行情数据统一接入、清洗与存储
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: pandas>=2.3.3
|
|
9
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
10
|
+
Provides-Extra: tushare
|
|
11
|
+
Requires-Dist: tushare>=1.4.29; extra == "tushare"
|
|
12
|
+
Provides-Extra: ricequant
|
|
13
|
+
Requires-Dist: rqdatac>=3.1.4; extra == "ricequant"
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# hqdata - A股历史与实时行情数据统一接入、清洗与存储
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img src ="https://img.shields.io/pypi/v/hqdata.svg"/>
|
|
23
|
+
<img src ="https://img.shields.io/pypi/pyversions/hqdata.svg"/>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
## 定位
|
|
27
|
+
|
|
28
|
+
`hqdata` 是 HonestQuant 量化系统的**数据基础层**,职责边界清晰:
|
|
29
|
+
|
|
30
|
+
- 对下:封装各数据源 SDK,屏蔽接口差异
|
|
31
|
+
- 对上:提供统一的查询接口
|
|
32
|
+
- 上层策略和引擎**只调用 `hqdata.api`**,不直接接触任何数据源
|
|
33
|
+
|
|
34
|
+
## 支持的数据源
|
|
35
|
+
|
|
36
|
+
| 数据源 | 状态 | 说明 |
|
|
37
|
+
| ----------- | ------ | ------------------------------------------- |
|
|
38
|
+
| **AKShare** | 计划中 | 免费,实时数据 |
|
|
39
|
+
| **Tushare** | 已接入 | 需满足账户积分要求,支持股票&指数的历史日线 |
|
|
40
|
+
| **米筐** | 接入中 | 需license,支持Tick |
|
|
41
|
+
| **迅投** | 计划中 | 需迅投终端 |
|
|
42
|
+
| **iTick** | 计划中 | 需注册 |
|
|
43
|
+
|
|
44
|
+
## 安装
|
|
45
|
+
|
|
46
|
+
### 方式一:通过pip安装
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# 基础安装(仅包含核心功能)
|
|
50
|
+
pip install hqdata
|
|
51
|
+
|
|
52
|
+
# 按需安装数据源依赖
|
|
53
|
+
pip install hqdata[tushare] # Tushare 支持
|
|
54
|
+
pip install hqdata[ricequant] # 米筐支持
|
|
55
|
+
pip install hqdata[tushare,ricequant] # 同时安装两者
|
|
56
|
+
|
|
57
|
+
# 复制模板并自行填入所需字段
|
|
58
|
+
cp .env.example .env # 放在你运行 Python 代码的当前目录(优先)/包安装目录
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 方式二:本地开发
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# 创建虚拟环境
|
|
65
|
+
python -m venv .venv
|
|
66
|
+
source .venv/bin/activate
|
|
67
|
+
|
|
68
|
+
# 安装依赖 (editable 模式,改代码直接生效)
|
|
69
|
+
pip install -e .
|
|
70
|
+
|
|
71
|
+
# 复制模板并自行填入所需字段
|
|
72
|
+
cp .env.example .env # 放在你运行 Python 代码的当前目录(优先)/包安装目录
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 使用
|
|
76
|
+
|
|
77
|
+
以Tushare数据为例
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from hqdata import init_source, get_bar
|
|
81
|
+
|
|
82
|
+
# 初始化 Tushare (日线数据)
|
|
83
|
+
init_source("tushare")
|
|
84
|
+
|
|
85
|
+
# 获取日线数据 (symbol 格式: "代码.交易所")
|
|
86
|
+
df = get_bar("600000.SH", frequency="1day", start_date="20260401", end_date="20260402")
|
|
87
|
+
print(df.head())
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 输入参数格式说明
|
|
91
|
+
|
|
92
|
+
### 股票代码
|
|
93
|
+
|
|
94
|
+
symbol 参数统一使用 `代码.交易所` 格式:
|
|
95
|
+
|
|
96
|
+
| 交易所 | 代码 | 示例 |
|
|
97
|
+
| ------ | ---- | ----------- |
|
|
98
|
+
| 上交所 | SH | `600000.SH` |
|
|
99
|
+
| 深交所 | SZ | `000001.SZ` |
|
|
100
|
+
|
|
101
|
+
## 测试
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pytest tests/ -v
|
|
105
|
+
pytest tests/test_tushare.py::TestTushareIntegration::test_get_bar # 运行单个测试
|
|
106
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
hqdata/__init__.py
|
|
5
|
+
hqdata/api.py
|
|
6
|
+
hqdata/config.py
|
|
7
|
+
hqdata.egg-info/PKG-INFO
|
|
8
|
+
hqdata.egg-info/SOURCES.txt
|
|
9
|
+
hqdata.egg-info/dependency_links.txt
|
|
10
|
+
hqdata.egg-info/requires.txt
|
|
11
|
+
hqdata.egg-info/top_level.txt
|
|
12
|
+
hqdata/sources/__init__.py
|
|
13
|
+
hqdata/sources/base.py
|
|
14
|
+
hqdata/sources/ricequant.py
|
|
15
|
+
hqdata/sources/tushare.py
|
|
16
|
+
tests/test_ricequant.py
|
|
17
|
+
tests/test_tushare.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hqdata
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hqdata"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "A股历史与实时行情数据统一接入、清洗与存储"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pandas>=2.3.3",
|
|
13
|
+
"python-dotenv>=1.0.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
tushare = [
|
|
18
|
+
"tushare>=1.4.29",
|
|
19
|
+
]
|
|
20
|
+
ricequant = [
|
|
21
|
+
"rqdatac>=3.1.4",
|
|
22
|
+
]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=7.0",
|
|
25
|
+
"pytest-cov>=4.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["."]
|
|
30
|
+
include = ["hqdata*"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
hqdata-0.1.2/setup.cfg
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Tests for ricequant source"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pytest
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import hqdata.config # 加载 .env
|
|
8
|
+
from hqdata.sources.ricequant import RicequantSource
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestRicequantSource:
|
|
12
|
+
"""Unit tests for RicequantSource."""
|
|
13
|
+
|
|
14
|
+
@patch.dict(os.environ, {}, clear=True)
|
|
15
|
+
def test_init_missing_credentials_raises(self):
|
|
16
|
+
with pytest.raises(ValueError, match="RQDATA"):
|
|
17
|
+
RicequantSource(username=None, password=None)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestRicequantIntegration:
|
|
21
|
+
"""Integration tests using real Ricequant API data."""
|
|
22
|
+
|
|
23
|
+
@pytest.fixture(autouse=True)
|
|
24
|
+
def setup(self):
|
|
25
|
+
token = os.getenv("RQDATA_USERNAME")
|
|
26
|
+
password = os.getenv("RQDATA_PASSWORD")
|
|
27
|
+
if not token or not password:
|
|
28
|
+
pytest.skip("RQDATA_USERNAME or RQDATA_PASSWORD not set")
|
|
29
|
+
self.source = RicequantSource(username=token, password=password)
|
|
30
|
+
|
|
31
|
+
def test_get_bar_not_implemented(self):
|
|
32
|
+
"""get_bar raises NotImplementedError until Ricequant adapter is implemented."""
|
|
33
|
+
with pytest.raises(NotImplementedError):
|
|
34
|
+
self.source.get_bar("600000.SH", "1day", "20260101", "20260101")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Tests for tushare source"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pytest
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
import hqdata.config # 加载 .env
|
|
9
|
+
from hqdata.sources.tushare import TushareSource
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestTushareSource:
|
|
13
|
+
"""Unit tests for TushareSource."""
|
|
14
|
+
|
|
15
|
+
@patch.dict(os.environ, {}, clear=True)
|
|
16
|
+
def test_init_missing_token_raises(self):
|
|
17
|
+
with pytest.raises(ValueError, match="TUSHARE_TOKEN"):
|
|
18
|
+
TushareSource(token=None)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestTushareIntegration:
|
|
22
|
+
"""Integration tests using real Tushare API data."""
|
|
23
|
+
|
|
24
|
+
@pytest.fixture(autouse=True)
|
|
25
|
+
def setup(self):
|
|
26
|
+
token = os.getenv("TUSHARE_TOKEN")
|
|
27
|
+
if not token:
|
|
28
|
+
pytest.skip("TUSHARE_TOKEN not set")
|
|
29
|
+
self.source = TushareSource(token=token)
|
|
30
|
+
|
|
31
|
+
def test_get_stock_list(self):
|
|
32
|
+
"""Test get_stock_list returns well-formed data for listed stocks."""
|
|
33
|
+
df = self.source.get_stock_list(list_status="L")
|
|
34
|
+
expected_columns = {"symbol", "name", "industry", "market", "exchange",
|
|
35
|
+
"curr_type", "list_status", "list_date", "delist_date", "is_hs"}
|
|
36
|
+
excluded_columns = {"ts_code", "area", "fullname", "enname", "cnspell", "act_name", "act_ent_type"}
|
|
37
|
+
|
|
38
|
+
assert not df.empty
|
|
39
|
+
assert expected_columns.issubset(df.columns), f"Missing columns: {expected_columns - set(df.columns)}"
|
|
40
|
+
assert not excluded_columns.intersection(df.columns), f"Unexpected columns present: {excluded_columns & set(df.columns)}"
|
|
41
|
+
assert (df["list_status"] == "L").all()
|
|
42
|
+
assert df["symbol"].is_unique
|
|
43
|
+
|
|
44
|
+
def test_get_bar(self):
|
|
45
|
+
"""Test get_bar returns well-formed data for both markets."""
|
|
46
|
+
expected_columns = {"symbol", "date", "open", "high", "low", "close",
|
|
47
|
+
"pre_close", "change", "pct_change", "volume", "amount"}
|
|
48
|
+
|
|
49
|
+
for symbol in ("000001.SZ", "600000.SH"):
|
|
50
|
+
df = self.source.get_bar(symbol, "1day", "20260101", "20260401")
|
|
51
|
+
assert not df.empty, f"{symbol} returned empty DataFrame"
|
|
52
|
+
assert expected_columns.issubset(df.columns), f"Missing columns: {expected_columns - set(df.columns)}"
|
|
53
|
+
assert (df["high"] >= df["low"]).all(), "high < low found"
|
|
54
|
+
assert (df["high"] >= df["close"]).all(), "high < close found"
|
|
55
|
+
assert (df["low"] <= df["close"]).all(), "low > close found"
|
|
56
|
+
assert (df["volume"] > 0).all(), "non-positive volume found"
|
|
57
|
+
assert (df["amount"] > 0).all(), "non-positive amount found"
|
|
58
|
+
|
|
59
|
+
def test_get_bar_unsupported_frequency(self):
|
|
60
|
+
"""Test that unsupported frequency raises NotImplementedError."""
|
|
61
|
+
with pytest.raises(NotImplementedError):
|
|
62
|
+
self.source.get_bar("000001.SZ", "1min", "20260101", "20260101")
|
|
63
|
+
|
|
64
|
+
def test_get_index_bar(self):
|
|
65
|
+
"""Test get_index_bar returns well-formed data for major indexes."""
|
|
66
|
+
expected_columns = {"symbol", "date", "open", "high", "low", "close",
|
|
67
|
+
"pre_close", "change", "pct_change", "volume", "amount"}
|
|
68
|
+
|
|
69
|
+
for symbol in ("000300.SH", "000905.SH", "000852.SH", "932000.CSI"):
|
|
70
|
+
df = self.source.get_index_bar(symbol, "20260101", "20260401")
|
|
71
|
+
print(df)
|
|
72
|
+
assert not df.empty, f"{symbol} returned empty DataFrame"
|
|
73
|
+
assert expected_columns.issubset(df.columns), f"Missing columns: {expected_columns - set(df.columns)}"
|
|
74
|
+
assert (df["high"] >= df["low"]).all(), "high < low found"
|
|
75
|
+
assert (df["high"] >= df["close"]).all(), "high < close found"
|
|
76
|
+
assert (df["low"] <= df["close"]).all(), "low > close found"
|
|
77
|
+
assert (df["volume"] > 0).all(), "non-positive volume found"
|
|
78
|
+
assert (df["amount"] > 0).all(), "non-positive amount found"
|