alpha-dc-common 0.1.23__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.
- alpha_dc_common-0.1.23/.gitignore +90 -0
- alpha_dc_common-0.1.23/PKG-INFO +6 -0
- alpha_dc_common-0.1.23/pyproject.toml +20 -0
- alpha_dc_common-0.1.23/pyproject.toml.backup +20 -0
- alpha_dc_common-0.1.23/src/dc_common/__init__.py +15 -0
- alpha_dc_common-0.1.23/src/dc_common/core/logger.py +48 -0
- alpha_dc_common-0.1.23/src/dc_common/data_sources/__init__.py +16 -0
- alpha_dc_common-0.1.23/src/dc_common/data_sources/base_source.py +176 -0
- alpha_dc_common-0.1.23/src/dc_common/data_sources/daily_quote_source.py +195 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/__init__.py +46 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/a_stock.py +24 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/base.py +45 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/hk_announcement.py +31 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/hk_company_profile.py +51 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/hk_stock.py +25 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/hs_industry.py +26 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/hs_industry_company.py +27 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/index_basic.py +26 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/index_company.py +19 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/index_daily.py +30 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/margin_account.py +29 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/margin_analysis.py +40 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/margin_detail.py +29 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/results.py +50 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/sw_index_daily.py +34 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/sw_industry.py +54 -0
- alpha_dc_common-0.1.23/src/dc_common/schemas/sw_industry_company.py +28 -0
- alpha_dc_common-0.1.23/src/dc_common/utils/auth_token.py +54 -0
- alpha_dc_common-0.1.23/src/dc_common/utils/baidu_utils.py +122 -0
- alpha_dc_common-0.1.23/src/dc_common/utils/cache_utils.py +141 -0
- alpha_dc_common-0.1.23/src/dc_common/utils/config_reader.py +79 -0
- alpha_dc_common-0.1.23/src/dc_common/utils/eastmoney_utils.py +118 -0
- alpha_dc_common-0.1.23/src/dc_common/utils/jin10_utils.py +153 -0
- alpha_dc_common-0.1.23/src/dc_common/utils/logging_config.py +104 -0
- alpha_dc_common-0.1.23/src/dc_common/utils/markdown_convert.py +121 -0
- alpha_dc_common-0.1.23/src/dc_common/utils/money_format.py +21 -0
- alpha_dc_common-0.1.23/src/dc_common/utils/proxy_utils.py +253 -0
- alpha_dc_common-0.1.23/src/dc_common/utils/schema_extract.py +179 -0
- alpha_dc_common-0.1.23/src/dc_common/utils/xueqiu_utils.py +253 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
pip-wheel-metadata/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.coverage
|
|
43
|
+
.coverage.*
|
|
44
|
+
.cache
|
|
45
|
+
nosetests.xml
|
|
46
|
+
coverage.xml
|
|
47
|
+
*.cover
|
|
48
|
+
*.py,cover
|
|
49
|
+
.hypothesis/
|
|
50
|
+
.pytest_cache/
|
|
51
|
+
|
|
52
|
+
# Jupyter Notebook
|
|
53
|
+
.ipynb_checkpoints
|
|
54
|
+
|
|
55
|
+
# IPython
|
|
56
|
+
profile_default/
|
|
57
|
+
ipython_config.py
|
|
58
|
+
|
|
59
|
+
# pyenv
|
|
60
|
+
.python-version
|
|
61
|
+
|
|
62
|
+
# PEP 582; __pypackages__
|
|
63
|
+
__pypackages__/
|
|
64
|
+
|
|
65
|
+
# Environments
|
|
66
|
+
.env
|
|
67
|
+
.venv
|
|
68
|
+
env/
|
|
69
|
+
venv/
|
|
70
|
+
ENV/
|
|
71
|
+
env.bak/
|
|
72
|
+
venv.bak/
|
|
73
|
+
|
|
74
|
+
# mypy
|
|
75
|
+
.mypy_cache/
|
|
76
|
+
.dmypy.json
|
|
77
|
+
dmypy.json
|
|
78
|
+
|
|
79
|
+
# uv lock file
|
|
80
|
+
uv.lock
|
|
81
|
+
|
|
82
|
+
# Log files
|
|
83
|
+
logs/
|
|
84
|
+
*.log
|
|
85
|
+
|
|
86
|
+
# Data directories (runtime generated data)
|
|
87
|
+
data/
|
|
88
|
+
|
|
89
|
+
# Project specific files
|
|
90
|
+
services/worker_service/data_fetchers/StockClassifyUse_stock.xls
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "alpha-dc-common"
|
|
7
|
+
version = "0.1.23"
|
|
8
|
+
description = "Shared schemas and utils for the datacenter project."
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Hang GuangLiang", email = "hanguangliang@alphaaidig.com"},
|
|
11
|
+
]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"pydantic>=2.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[tool.hatch.build.targets.wheel]
|
|
17
|
+
packages = ["src/dc_common"]
|
|
18
|
+
|
|
19
|
+
[tool.uv]
|
|
20
|
+
index-url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dc_common"
|
|
7
|
+
version = "0.1.23"
|
|
8
|
+
description = "Shared schemas and utils for the datacenter project."
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Hang GuangLiang", email = "hanguangliang@alphaaidig.com"},
|
|
11
|
+
]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"pydantic>=2.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[tool.hatch.build.targets.wheel]
|
|
17
|
+
packages = ["src/dc_common"]
|
|
18
|
+
|
|
19
|
+
[tool.uv]
|
|
20
|
+
index-url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DataCenter Common - 公共库
|
|
3
|
+
|
|
4
|
+
提供:
|
|
5
|
+
- 数据源(Tushare、本地缓存等)
|
|
6
|
+
- 工具函数
|
|
7
|
+
- 核心组件
|
|
8
|
+
"""
|
|
9
|
+
from .data_sources import BaseDataSource, DataSourceStatus, DailyQuoteSource
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'BaseDataSource',
|
|
13
|
+
'DataSourceStatus',
|
|
14
|
+
'DailyQuoteSource',
|
|
15
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
def setup_logger(name: str = "datacenter") -> logging.Logger:
|
|
7
|
+
"""设置并返回配置好的logger实例"""
|
|
8
|
+
|
|
9
|
+
# 区分开发和生产环境
|
|
10
|
+
if os.environ.get('ENVIRONMENT') == 'production':
|
|
11
|
+
# 生产环境使用标准日志目录
|
|
12
|
+
log_dir = Path("/var/log/datacenter")
|
|
13
|
+
log_dir.mkdir(exist_ok=True, mode=0o755)
|
|
14
|
+
else:
|
|
15
|
+
# 开发环境使用相对路径
|
|
16
|
+
log_dir = Path("logs")
|
|
17
|
+
log_dir.mkdir(exist_ok=True)
|
|
18
|
+
|
|
19
|
+
# 创建logger
|
|
20
|
+
logger = logging.getLogger(name)
|
|
21
|
+
logger.setLevel(logging.INFO)
|
|
22
|
+
|
|
23
|
+
# 清除现有的handlers
|
|
24
|
+
logger.handlers.clear()
|
|
25
|
+
|
|
26
|
+
# 创建formatter
|
|
27
|
+
formatter = logging.Formatter(
|
|
28
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# 文件handler
|
|
32
|
+
file_handler = logging.FileHandler(log_dir / "app.log", encoding="utf-8")
|
|
33
|
+
file_handler.setLevel(logging.INFO)
|
|
34
|
+
file_handler.setFormatter(formatter)
|
|
35
|
+
|
|
36
|
+
# 控制台handler
|
|
37
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
38
|
+
console_handler.setLevel(logging.INFO)
|
|
39
|
+
console_handler.setFormatter(formatter)
|
|
40
|
+
|
|
41
|
+
# 添加handlers
|
|
42
|
+
logger.addHandler(file_handler)
|
|
43
|
+
logger.addHandler(console_handler)
|
|
44
|
+
|
|
45
|
+
return logger
|
|
46
|
+
|
|
47
|
+
# 创建默认logger实例
|
|
48
|
+
logger = setup_logger()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
数据源模块 - 提供各类数据源的实现
|
|
3
|
+
|
|
4
|
+
本模块提供统一的数据源接口,支持:
|
|
5
|
+
- Tushare 数据源
|
|
6
|
+
- 本地缓存管理
|
|
7
|
+
- 数据质量验证
|
|
8
|
+
"""
|
|
9
|
+
from .base_source import BaseDataSource, DataSourceStatus
|
|
10
|
+
from .daily_quote_source import DailyQuoteSource
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'BaseDataSource',
|
|
14
|
+
'DataSourceStatus',
|
|
15
|
+
'DailyQuoteSource'
|
|
16
|
+
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
数据源基类定义
|
|
3
|
+
"""
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from datetime import datetime, time
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DataSourceStatus(Enum):
|
|
14
|
+
"""数据源状态"""
|
|
15
|
+
PENDING = "pending" # 等待中
|
|
16
|
+
READY = "ready" # 数据就绪
|
|
17
|
+
UPDATING = "updating" # 更新中
|
|
18
|
+
FAILED = "failed" # 更新失败
|
|
19
|
+
NOT_NEEDED = "not_needed" # 该日不需要更新
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BaseDataSource(ABC):
|
|
23
|
+
"""数据源基类
|
|
24
|
+
|
|
25
|
+
每个数据源有独立的调度策略和更新时间
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, data_dir: str, logger: Optional[logging.Logger] = None):
|
|
29
|
+
self.data_dir = Path(data_dir)
|
|
30
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def source_name(self) -> str:
|
|
36
|
+
"""数据源唯一标识"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def display_name(self) -> str:
|
|
42
|
+
"""显示名称"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def update_time(self) -> str:
|
|
48
|
+
"""
|
|
49
|
+
期望更新时间
|
|
50
|
+
例如: "17:00", "09:30"
|
|
51
|
+
"""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def update_delay_days(self) -> int:
|
|
56
|
+
"""
|
|
57
|
+
更新延迟天数
|
|
58
|
+
0 = 当天更新
|
|
59
|
+
1 = 次日更新
|
|
60
|
+
"""
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def priority(self) -> int:
|
|
65
|
+
"""
|
|
66
|
+
优先级(数字越小越优先)
|
|
67
|
+
基础数据优先级高,衍生数据优先级低
|
|
68
|
+
"""
|
|
69
|
+
return 100
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def fetch_data(self, trade_date: str) -> pd.DataFrame:
|
|
73
|
+
"""
|
|
74
|
+
获取数据
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
trade_date: 交易日期 (YYYYMMDD)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
数据 DataFrame
|
|
81
|
+
"""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def validate_data(self, df: pd.DataFrame) -> bool:
|
|
86
|
+
"""
|
|
87
|
+
验证数据质量
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
True=数据有效, False=数据无效
|
|
91
|
+
"""
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
def is_ready(self, trade_date: str) -> DataSourceStatus:
|
|
95
|
+
"""
|
|
96
|
+
检查数据是否就绪
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
trade_date: 交易日期
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
数据源状态
|
|
103
|
+
"""
|
|
104
|
+
# 1. 检查本地是否已有数据
|
|
105
|
+
if self._has_local_data(trade_date):
|
|
106
|
+
return DataSourceStatus.READY
|
|
107
|
+
|
|
108
|
+
# 2. 检查是否到了更新时间
|
|
109
|
+
if not self._is_update_time():
|
|
110
|
+
return DataSourceStatus.PENDING
|
|
111
|
+
|
|
112
|
+
return DataSourceStatus.READY
|
|
113
|
+
|
|
114
|
+
def update(self, trade_date: str) -> Dict[str, Any]:
|
|
115
|
+
"""
|
|
116
|
+
更新数据
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
trade_date: 交易日期
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
更新结果字典
|
|
123
|
+
"""
|
|
124
|
+
result = {
|
|
125
|
+
'source': self.source_name,
|
|
126
|
+
'trade_date': trade_date,
|
|
127
|
+
'status': 'unknown',
|
|
128
|
+
'rows': 0,
|
|
129
|
+
'message': ''
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
self.logger.info(f"🔄 开始更新 {self.display_name}: {trade_date}")
|
|
134
|
+
|
|
135
|
+
# 1. 获取数据
|
|
136
|
+
df = self.fetch_data(trade_date)
|
|
137
|
+
|
|
138
|
+
# 2. 验证数据
|
|
139
|
+
if not self.validate_data(df):
|
|
140
|
+
result['status'] = 'failed'
|
|
141
|
+
result['message'] = '数据验证失败'
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
# 3. 保存数据
|
|
145
|
+
self._save_data(df, trade_date)
|
|
146
|
+
|
|
147
|
+
result['status'] = 'success'
|
|
148
|
+
result['rows'] = len(df)
|
|
149
|
+
result['message'] = f'成功更新 {len(df)} 条数据'
|
|
150
|
+
|
|
151
|
+
self.logger.info(f"✅ {self.display_name} 更新完成: {len(df)} 条")
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
result['status'] = 'failed'
|
|
155
|
+
result['message'] = str(e)
|
|
156
|
+
self.logger.error(f"❌ {self.display_name} 更新失败: {e}")
|
|
157
|
+
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
def _is_update_time(self) -> bool:
|
|
161
|
+
"""检查是否到了更新时间"""
|
|
162
|
+
try:
|
|
163
|
+
hour, minute = map(int, self.update_time.split(':'))
|
|
164
|
+
target_time = time(hour, minute)
|
|
165
|
+
now = datetime.now().time()
|
|
166
|
+
return now >= target_time
|
|
167
|
+
except:
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
def _has_local_data(self, trade_date: str) -> bool:
|
|
171
|
+
"""检查本地是否已有数据(子类覆盖)"""
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
def _save_data(self, df: pd.DataFrame, trade_date: str):
|
|
175
|
+
"""保存数据(子类实现)"""
|
|
176
|
+
pass
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
日线行情数据源
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from .base_source import BaseDataSource, DataSourceStatus
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DailyQuoteSource(BaseDataSource):
|
|
14
|
+
"""日线行情数据源
|
|
15
|
+
|
|
16
|
+
从 Tushare 获取日线行情数据
|
|
17
|
+
更新时间:每天 17:00
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
data_dir: str,
|
|
23
|
+
tushare_token: Optional[str] = None,
|
|
24
|
+
logger: Optional[logging.Logger] = None
|
|
25
|
+
):
|
|
26
|
+
super().__init__(data_dir, logger)
|
|
27
|
+
|
|
28
|
+
self.token = tushare_token or os.getenv('TUSHARE_TOKEN')
|
|
29
|
+
if not self.token:
|
|
30
|
+
raise ValueError("Tushare token 未配置,请设置 TUSHARE_TOKEN 环境变量")
|
|
31
|
+
|
|
32
|
+
# 缓存文件直接存储在 data_dir 下(不再添加 raw 子目录)
|
|
33
|
+
self.cache_file = self.data_dir / 'daily_quotes.parquet'
|
|
34
|
+
self.raw_cache_file = self.data_dir / 'daily_quotes_raw.parquet'
|
|
35
|
+
|
|
36
|
+
# 延迟初始化 Tushare(避免导入时立即连接)
|
|
37
|
+
self._pro = None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def pro(self):
|
|
41
|
+
"""延迟初始化 Tushare Pro API"""
|
|
42
|
+
if self._pro is None:
|
|
43
|
+
import tushare as ts
|
|
44
|
+
ts.set_token(self.token)
|
|
45
|
+
self._pro = ts.pro_api()
|
|
46
|
+
return self._pro
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def source_name(self) -> str:
|
|
50
|
+
return "daily_quotes"
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def display_name(self) -> str:
|
|
54
|
+
return "日线行情"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def update_time(self) -> str:
|
|
58
|
+
return "17:00"
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def update_delay_days(self) -> int:
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def priority(self) -> int:
|
|
66
|
+
return 10
|
|
67
|
+
|
|
68
|
+
def fetch_data(self, trade_date: str) -> pd.DataFrame:
|
|
69
|
+
"""从 Tushare 获取日线数据"""
|
|
70
|
+
self.logger.info(f"从 Tushare 获取 {trade_date} 的日线数据...")
|
|
71
|
+
|
|
72
|
+
df = self.pro.daily(
|
|
73
|
+
trade_date=trade_date
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if df.empty:
|
|
77
|
+
self.logger.warning(f"{trade_date} 无数据(可能是非交易日)")
|
|
78
|
+
else:
|
|
79
|
+
self.logger.info(f"获取到 {len(df)} 条数据")
|
|
80
|
+
|
|
81
|
+
return df
|
|
82
|
+
|
|
83
|
+
def validate_data(self, df: pd.DataFrame) -> bool:
|
|
84
|
+
"""验证数据质量"""
|
|
85
|
+
if df.empty:
|
|
86
|
+
return True # 空数据也是有效的(可能是非交易日)
|
|
87
|
+
|
|
88
|
+
required_fields = ['ts_code', 'trade_date', 'close']
|
|
89
|
+
return all(field in df.columns for field in required_fields)
|
|
90
|
+
|
|
91
|
+
def _has_local_data(self, trade_date: str) -> bool:
|
|
92
|
+
"""检查本地是否已有数据"""
|
|
93
|
+
if not self.raw_cache_file.exists():
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
df = pd.read_parquet(self.raw_cache_file)
|
|
98
|
+
return trade_date in df['trade_date'].values
|
|
99
|
+
except Exception as e:
|
|
100
|
+
self.logger.warning(f"检查本地数据失败: {e}")
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def _save_data(self, df: pd.DataFrame, trade_date: str):
|
|
104
|
+
"""保存数据(追加模式)"""
|
|
105
|
+
# 确保目录存在
|
|
106
|
+
self.raw_cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
self.cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
|
|
109
|
+
# 保存原始长格式数据
|
|
110
|
+
if self.raw_cache_file.exists():
|
|
111
|
+
existing_df = pd.read_parquet(self.raw_cache_file)
|
|
112
|
+
|
|
113
|
+
# 移除旧的同日期数据(如果有)
|
|
114
|
+
existing_df = existing_df[existing_df['trade_date'] != trade_date]
|
|
115
|
+
|
|
116
|
+
# 合并数据
|
|
117
|
+
combined_df = pd.concat([existing_df, df], ignore_index=True)
|
|
118
|
+
|
|
119
|
+
# 去重(基于 trade_date 和 ts_code,保留后出现的)
|
|
120
|
+
combined_df = combined_df.drop_duplicates(
|
|
121
|
+
subset=['trade_date', 'ts_code'],
|
|
122
|
+
keep='last'
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
combined_df = df
|
|
126
|
+
|
|
127
|
+
combined_df.to_parquet(self.raw_cache_file, compression='snappy')
|
|
128
|
+
self.logger.info(f"原始数据已保存: {self.raw_cache_file}")
|
|
129
|
+
|
|
130
|
+
# 转换并保存宽表格式(用于因子计算)
|
|
131
|
+
self._save_wide_format(combined_df)
|
|
132
|
+
|
|
133
|
+
def _save_wide_format(self, df: pd.DataFrame):
|
|
134
|
+
"""转换为宽表格式并保存(增量追加)"""
|
|
135
|
+
if df.empty:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# 排序
|
|
139
|
+
df = df.sort_values(['trade_date', 'ts_code'])
|
|
140
|
+
|
|
141
|
+
# 如果文件已存在,进行增量追加
|
|
142
|
+
if self.cache_file.exists():
|
|
143
|
+
try:
|
|
144
|
+
# 读取已有数据
|
|
145
|
+
existing_df = pd.read_parquet(self.cache_file)
|
|
146
|
+
|
|
147
|
+
# 移除与新数据重复的日期
|
|
148
|
+
existing_dates = existing_df['trade_date'].unique()
|
|
149
|
+
new_dates = df['trade_date'].unique()
|
|
150
|
+
|
|
151
|
+
# 只保留新数据中没有的旧日期
|
|
152
|
+
dates_to_keep = set(existing_dates) - set(new_dates)
|
|
153
|
+
if dates_to_keep:
|
|
154
|
+
existing_df = existing_df[existing_df['trade_date'].isin(dates_to_keep)]
|
|
155
|
+
else:
|
|
156
|
+
existing_df = pd.DataFrame()
|
|
157
|
+
|
|
158
|
+
# 合并数据
|
|
159
|
+
if not existing_df.empty:
|
|
160
|
+
combined_df = pd.concat([existing_df, df], ignore_index=True)
|
|
161
|
+
else:
|
|
162
|
+
combined_df = df
|
|
163
|
+
|
|
164
|
+
# 去重(基于 trade_date 和 ts_code,保留后出现的)
|
|
165
|
+
combined_df = combined_df.drop_duplicates(
|
|
166
|
+
subset=['trade_date', 'ts_code'],
|
|
167
|
+
keep='last'
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# 排序
|
|
171
|
+
combined_df = combined_df.sort_values(['trade_date', 'ts_code'])
|
|
172
|
+
|
|
173
|
+
# 保存
|
|
174
|
+
combined_df.to_parquet(self.cache_file, compression='snappy', index=False)
|
|
175
|
+
|
|
176
|
+
old_len = len(existing_df) if dates_to_keep else 0
|
|
177
|
+
new_len = len(combined_df)
|
|
178
|
+
added_rows = new_len - old_len
|
|
179
|
+
|
|
180
|
+
self.logger.info(
|
|
181
|
+
f"宽表数据已增量更新: {self.cache_file} "
|
|
182
|
+
f"(原有: {old_len}, 新增: {added_rows}, 总计: {new_len})"
|
|
183
|
+
)
|
|
184
|
+
except Exception as e:
|
|
185
|
+
self.logger.warning(f"增量更新失败: {e},执行覆盖保存")
|
|
186
|
+
df.to_parquet(self.cache_file, compression='snappy', index=False)
|
|
187
|
+
self.logger.info(f"宽表数据已保存: {self.cache_file}")
|
|
188
|
+
else:
|
|
189
|
+
# 首次保存(先去重)
|
|
190
|
+
df = df.drop_duplicates(subset=['trade_date', 'ts_code'], keep='last')
|
|
191
|
+
df.to_parquet(self.cache_file, compression='snappy', index=False)
|
|
192
|
+
self.logger.info(f"宽表数据已保存: {self.cache_file}")
|
|
193
|
+
|
|
194
|
+
self.logger.info(f"数据范围: {df['trade_date'].min()} ~ {df['trade_date'].max()}")
|
|
195
|
+
self.logger.info(f"股票数量: {df['ts_code'].nunique()}")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
数据中心通用schemas模块
|
|
3
|
+
"""
|
|
4
|
+
from .base import BaseResponse, PaginatedResponse, PaginationInfo
|
|
5
|
+
from .results import ResultType, BaseResult, TableResult, PageResult, DictResult, SingleResult
|
|
6
|
+
from .a_stock import AStock
|
|
7
|
+
from .hk_stock import HKStock
|
|
8
|
+
from .index_basic import IndexBasic
|
|
9
|
+
from .index_company import IndexCompany
|
|
10
|
+
from .margin_account import MarginAccount
|
|
11
|
+
from .margin_analysis import MarginAnalysis
|
|
12
|
+
from .margin_detail import MarginDetail
|
|
13
|
+
|
|
14
|
+
from .hs_industry import HSIndustry, HSIndustryCategory
|
|
15
|
+
from .hs_industry_company import HSIndustryCompany
|
|
16
|
+
from .sw_industry import SWIndustry
|
|
17
|
+
from .sw_industry_company import SWIndustryCompany
|
|
18
|
+
from .index_daily import IndexDaily
|
|
19
|
+
from .sw_index_daily import SWIndexDaily
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"BaseResponse",
|
|
23
|
+
"PaginatedResponse",
|
|
24
|
+
"PaginationInfo",
|
|
25
|
+
"ResultType",
|
|
26
|
+
"BaseResult",
|
|
27
|
+
"TableResult",
|
|
28
|
+
"PageResult",
|
|
29
|
+
"DictResult",
|
|
30
|
+
"SingleResult",
|
|
31
|
+
"AStock",
|
|
32
|
+
"HKStock",
|
|
33
|
+
"IndexBasic",
|
|
34
|
+
"IndexCompany",
|
|
35
|
+
"MarginAccount",
|
|
36
|
+
"MarginAnalysis",
|
|
37
|
+
"MarginDetail",
|
|
38
|
+
|
|
39
|
+
"HSIndustry",
|
|
40
|
+
"HSIndustryCategory",
|
|
41
|
+
"HSIndustryCompany",
|
|
42
|
+
"SWIndustry",
|
|
43
|
+
"SWIndustryCompany",
|
|
44
|
+
"IndexDaily",
|
|
45
|
+
"SWIndexDaily",
|
|
46
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A股相关的Pydantic模型
|
|
3
|
+
"""
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
class AStockBase(BaseModel):
|
|
9
|
+
stock_code: str = Field(..., description="股票代码")
|
|
10
|
+
stock_name: str = Field(..., description="股票名称")
|
|
11
|
+
exchange: str = Field(..., description="交易所(SH/SZ/BJ)")
|
|
12
|
+
list_status: Optional[str] = Field(None, description="上市状态(L上市/D退市/P暂停上市)")
|
|
13
|
+
list_date: Optional[datetime] = Field(None, description="上市日期")
|
|
14
|
+
curr_type: Optional[str] = Field(None, description="交易货币(CNY)")
|
|
15
|
+
market: Optional[str] = Field(None, description="市场类型(主板/创业板/科创板/北交所)")
|
|
16
|
+
ric_code: Optional[str] = Field(None, description="路透代码")
|
|
17
|
+
|
|
18
|
+
class AStock(AStockBase):
|
|
19
|
+
id: int
|
|
20
|
+
created_at: datetime
|
|
21
|
+
updated_at: datetime
|
|
22
|
+
|
|
23
|
+
class Config:
|
|
24
|
+
from_attributes = True
|