cmeshare 0.1.0__py3-none-any.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.
- cmeshare/__init__.py +14 -0
- cmeshare/historydata.py +188 -0
- cmeshare-0.1.0.dist-info/METADATA +15 -0
- cmeshare-0.1.0.dist-info/RECORD +6 -0
- cmeshare-0.1.0.dist-info/WHEEL +5 -0
- cmeshare-0.1.0.dist-info/top_level.txt +1 -0
cmeshare/__init__.py
ADDED
cmeshare/historydata.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""CMEShare 历史数据接口(精简版)。
|
|
3
|
+
|
|
4
|
+
约定:数据目录与 key 同名,请求 HQ:
|
|
5
|
+
POST /api/down_file_by_token { token, key, filename }
|
|
6
|
+
落盘:DATA_ROOT/{key}/{code}.zip
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import io
|
|
10
|
+
import os
|
|
11
|
+
import zipfile
|
|
12
|
+
from datetime import date, datetime
|
|
13
|
+
|
|
14
|
+
import pandas as pd
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HistoryDataError(Exception):
|
|
19
|
+
"""历史数据错误。"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RateLimitError(HistoryDataError):
|
|
23
|
+
def __init__(self, message='', status_code=429):
|
|
24
|
+
super().__init__(message or '访问过于频繁,请降低请求频率!')
|
|
25
|
+
self.status_code = status_code
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _hq_url():
|
|
29
|
+
return (os.environ.get('CMESHARE_HQ_URL') or 'http://127.0.0.1:5000').rstrip('/')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _require_token(token_str=''):
|
|
33
|
+
tok = (token_str or '').strip()
|
|
34
|
+
if not tok:
|
|
35
|
+
raise ValueError('请提供 token_str')
|
|
36
|
+
return tok
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _as_zip_name(name):
|
|
40
|
+
name = str(name or '').strip()
|
|
41
|
+
if not name:
|
|
42
|
+
return ''
|
|
43
|
+
return name if name.lower().endswith('.zip') else f'{name}.zip'
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_date(value):
|
|
47
|
+
if value is None or (isinstance(value, str) and not str(value).strip()):
|
|
48
|
+
raise ValueError('日期不能为空')
|
|
49
|
+
if isinstance(value, datetime):
|
|
50
|
+
return value.date()
|
|
51
|
+
if isinstance(value, date):
|
|
52
|
+
return value
|
|
53
|
+
s = str(value).strip().replace('/', '-').replace('.', '-')
|
|
54
|
+
if len(s) == 8 and s.isdigit():
|
|
55
|
+
return datetime.strptime(s, '%Y%m%d').date()
|
|
56
|
+
if ' ' in s:
|
|
57
|
+
s = s.split(' ', 1)[0]
|
|
58
|
+
if 'T' in s:
|
|
59
|
+
s = s.split('T', 1)[0]
|
|
60
|
+
return datetime.strptime(s, '%Y-%m-%d').date()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _time_col(df):
|
|
64
|
+
for col in ('时间', '交易日期', '日期', 'datetime', 'date', 'Date', 'DATE'):
|
|
65
|
+
if col in df.columns:
|
|
66
|
+
return col
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _filter_df_by_date(df, start_date, end_date):
|
|
71
|
+
if df is None or df.empty:
|
|
72
|
+
return pd.DataFrame() if df is None else df
|
|
73
|
+
col = _time_col(df)
|
|
74
|
+
if not col:
|
|
75
|
+
return df
|
|
76
|
+
start_d = _parse_date(start_date)
|
|
77
|
+
end_d = _parse_date(end_date)
|
|
78
|
+
if start_d > end_d:
|
|
79
|
+
start_d, end_d = end_d, start_d
|
|
80
|
+
start_ts = pd.Timestamp(start_d)
|
|
81
|
+
end_ts = pd.Timestamp(end_d) + pd.Timedelta(days=1) - pd.Timedelta(microseconds=1)
|
|
82
|
+
ts = pd.to_datetime(df[col], errors='coerce')
|
|
83
|
+
mask = (ts >= start_ts) & (ts <= end_ts)
|
|
84
|
+
return df.loc[mask].reset_index(drop=True)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _read_csv_bytes(raw):
|
|
88
|
+
last_err = None
|
|
89
|
+
for enc in ('utf-8-sig', 'utf-8', 'gbk'):
|
|
90
|
+
try:
|
|
91
|
+
return pd.read_csv(io.BytesIO(raw), encoding=enc, low_memory=False)
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
last_err = exc
|
|
94
|
+
raise HistoryDataError(f'CSV 解码失败: {last_err}') from last_err
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _zip_bytes_to_df(content):
|
|
98
|
+
if not content:
|
|
99
|
+
raise HistoryDataError('下载内容为空')
|
|
100
|
+
try:
|
|
101
|
+
with zipfile.ZipFile(io.BytesIO(content)) as zf:
|
|
102
|
+
for name in zf.namelist():
|
|
103
|
+
if name.endswith('/') or name.startswith('__MACOSX'):
|
|
104
|
+
continue
|
|
105
|
+
if name.lower().endswith('.csv'):
|
|
106
|
+
return _read_csv_bytes(zf.read(name))
|
|
107
|
+
except HistoryDataError:
|
|
108
|
+
raise
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
raise HistoryDataError(f'zip 解析失败: {exc}') from exc
|
|
111
|
+
raise HistoryDataError('zip 内未找到 csv 文件')
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _is_rate_limited(status_code=None, err_msg=''):
|
|
115
|
+
if status_code == 429:
|
|
116
|
+
return True
|
|
117
|
+
text = str(err_msg or '')
|
|
118
|
+
return ('频繁' in text) or ('请求处理中' in text)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _download_file(key, filename, token, folder=''):
|
|
122
|
+
"""调用 HQ:key 即数据目录名。"""
|
|
123
|
+
payload = {
|
|
124
|
+
'key': key,
|
|
125
|
+
'filename': filename,
|
|
126
|
+
'folder': str(folder or ''),
|
|
127
|
+
'token': token,
|
|
128
|
+
}
|
|
129
|
+
resp = requests.post(
|
|
130
|
+
f'{_hq_url()}/api/down_file_by_token',
|
|
131
|
+
json=payload,
|
|
132
|
+
headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {token}'},
|
|
133
|
+
timeout=600,
|
|
134
|
+
)
|
|
135
|
+
ctype = (resp.headers.get('Content-Type') or '').lower()
|
|
136
|
+
if 'application/json' in ctype:
|
|
137
|
+
try:
|
|
138
|
+
data = resp.json()
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
raise HistoryDataError(f'下载失败 HTTP {resp.status_code}: {exc}') from exc
|
|
141
|
+
err = str((data or {}).get('error') or (data or {}).get('message') or resp.text or '')
|
|
142
|
+
if _is_rate_limited(resp.status_code, err):
|
|
143
|
+
raise RateLimitError(err, status_code=resp.status_code)
|
|
144
|
+
raise HistoryDataError(err or f'下载失败 HTTP {resp.status_code}')
|
|
145
|
+
if not resp.ok:
|
|
146
|
+
err = (resp.text or '')[:500]
|
|
147
|
+
if _is_rate_limited(resp.status_code, err):
|
|
148
|
+
raise RateLimitError(err, status_code=resp.status_code)
|
|
149
|
+
raise HistoryDataError(err or f'下载失败 HTTP {resp.status_code}')
|
|
150
|
+
return resp.content
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_historical_data(key, code, start_date=None, end_date=None, token_str='', adjust='adj_none'):
|
|
154
|
+
"""下载历史行情并返回 DataFrame。
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
key: 数据目录名,如 split_daily / a_index_day
|
|
158
|
+
code: 标的代码,如 SH.600000
|
|
159
|
+
start_date / end_date: 可选,按「时间」列裁剪
|
|
160
|
+
token_str: 个人 token(必填)
|
|
161
|
+
adjust: 暂仅支持 adj_none(占位,与文档参数兼容)
|
|
162
|
+
"""
|
|
163
|
+
key = (key or '').strip()
|
|
164
|
+
code = (code or '').strip()
|
|
165
|
+
if not key:
|
|
166
|
+
raise ValueError('key 不能为空')
|
|
167
|
+
if not code:
|
|
168
|
+
raise ValueError('code 不能为空')
|
|
169
|
+
|
|
170
|
+
adjust_key = (adjust or 'adj_none').strip().lower()
|
|
171
|
+
if adjust_key not in ('', 'adj_none', 'none'):
|
|
172
|
+
raise ValueError('当前仅支持 adjust=adj_none')
|
|
173
|
+
|
|
174
|
+
token = _require_token(token_str)
|
|
175
|
+
zip_name = _as_zip_name(code)
|
|
176
|
+
content = _download_file(key, zip_name, token)
|
|
177
|
+
df = _zip_bytes_to_df(content)
|
|
178
|
+
|
|
179
|
+
has_start = start_date is not None and str(start_date).strip() != ''
|
|
180
|
+
has_end = end_date is not None and str(end_date).strip() != ''
|
|
181
|
+
if has_start or has_end:
|
|
182
|
+
start_s = str(start_date).strip() if has_start else str(end_date).strip()
|
|
183
|
+
end_s = str(end_date).strip() if has_end else start_s
|
|
184
|
+
df = _filter_df_by_date(df, start_s, end_s)
|
|
185
|
+
|
|
186
|
+
if df is None or df.empty:
|
|
187
|
+
raise HistoryDataError(f'未获取到数据:{key} {code}')
|
|
188
|
+
return df
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cmeshare
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CMEShare 免费行情 Python 接口
|
|
5
|
+
Author: CMEShare
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Requires-Dist: pandas>=1.3.0
|
|
10
|
+
Requires-Dist: requests>=2.25.0
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: classifier
|
|
13
|
+
Dynamic: requires-dist
|
|
14
|
+
Dynamic: requires-python
|
|
15
|
+
Dynamic: summary
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
cmeshare/__init__.py,sha256=WRA5ZBE9FJGlk4L8-eBZN5jRPGyCeN2ZZ9VX_Ds4ZtI,252
|
|
2
|
+
cmeshare/historydata.py,sha256=mKGevBaDXsWzV0Ipa2ajU6LQT4Vk6PHEMQ3APXc04Js,6246
|
|
3
|
+
cmeshare-0.1.0.dist-info/METADATA,sha256=rVJ35Z3z7Kr6kuoOUmmhAW171-TfvFFgz4OLu7fLm-g,409
|
|
4
|
+
cmeshare-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
cmeshare-0.1.0.dist-info/top_level.txt,sha256=BKIIyUcRPUNECtthqPq65-qw7XHExGg22Mc6gpxXnLU,9
|
|
6
|
+
cmeshare-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cmeshare
|