ffdata 0.2.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.
- ffdata/__init__.py +162 -0
- ffdata/cli.py +46 -0
- ffdata/client.py +554 -0
- ffdata/datasets/__init__.py +5 -0
- ffdata/datasets/manager.py +132 -0
- ffdata/local/__init__.py +5 -0
- ffdata/local/store.py +146 -0
- ffdata/transport/__init__.py +5 -0
- ffdata/transport/http.py +175 -0
- ffdata-0.2.0.data/scripts/configure_ffdata.sh +59 -0
- ffdata-0.2.0.dist-info/METADATA +32 -0
- ffdata-0.2.0.dist-info/RECORD +22 -0
- ffdata-0.2.0.dist-info/WHEEL +5 -0
- ffdata-0.2.0.dist-info/entry_points.txt +2 -0
- ffdata-0.2.0.dist-info/top_level.txt +2 -0
- ffdata_common/__init__.py +6 -0
- ffdata_common/codecs/__init__.py +5 -0
- ffdata_common/codecs/arrow.py +26 -0
- ffdata_common/errors.py +30 -0
- ffdata_common/protocol.py +18 -0
- ffdata_common/schemas/__init__.py +5 -0
- ffdata_common/schemas/manifest.py +52 -0
ffdata/client.py
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
"""研究员使用的 ffdata Python 客户端。
|
|
2
|
+
|
|
3
|
+
客户端统一封装远程 HTTP 查询、本地 Parquet 查询和自动路由,并向上保持稳定的
|
|
4
|
+
``pandas.DataFrame`` 返回契约。
|
|
5
|
+
"""
|
|
6
|
+
# ruff: noqa: E501
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
import pandas as pd
|
|
16
|
+
from ffdata_common.errors import DatasetError
|
|
17
|
+
from ffdata_common.protocol import LOCAL_DATASET_APIS
|
|
18
|
+
|
|
19
|
+
from .datasets import DatasetManager
|
|
20
|
+
from .local import LocalStore
|
|
21
|
+
from .transport import HttpTransport
|
|
22
|
+
|
|
23
|
+
Source = Literal["remote", "local", "auto"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Client:
|
|
27
|
+
"""ffdata 主客户端,聚合远程传输、本地存储和数据集同步能力。"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
*,
|
|
32
|
+
base_url: str = "http://127.0.0.1:8000",
|
|
33
|
+
token: str | None = None,
|
|
34
|
+
data_dir: str | Path | None = None,
|
|
35
|
+
timeout: float = 300.0,
|
|
36
|
+
http_client: httpx.Client | None = None,
|
|
37
|
+
):
|
|
38
|
+
root = Path(
|
|
39
|
+
data_dir or os.getenv("FFDATA_DATA_DIR", str(Path.home() / ".ffdata" / "datasets"))
|
|
40
|
+
)
|
|
41
|
+
self.transport = HttpTransport(
|
|
42
|
+
base_url=base_url,
|
|
43
|
+
token=token,
|
|
44
|
+
timeout=timeout,
|
|
45
|
+
client=http_client,
|
|
46
|
+
)
|
|
47
|
+
self.local = LocalStore(root)
|
|
48
|
+
self.datasets = DatasetManager(self.transport, self.local)
|
|
49
|
+
|
|
50
|
+
def close(self) -> None:
|
|
51
|
+
self.transport.close()
|
|
52
|
+
|
|
53
|
+
def __enter__(self) -> Client:
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
def __exit__(self, *_: Any) -> None:
|
|
57
|
+
self.close()
|
|
58
|
+
|
|
59
|
+
def query(
|
|
60
|
+
self,
|
|
61
|
+
api_name: str,
|
|
62
|
+
*,
|
|
63
|
+
fields: list[str] | None = None,
|
|
64
|
+
source: Source = "remote",
|
|
65
|
+
response_format: Literal["json", "arrow"] = "arrow",
|
|
66
|
+
**params: Any,
|
|
67
|
+
) -> pd.DataFrame:
|
|
68
|
+
"""执行通用查询,并根据 ``source`` 选择远程或本地数据源。"""
|
|
69
|
+
if source not in {"remote", "local", "auto"}:
|
|
70
|
+
raise ValueError("source must be 'remote', 'local', or 'auto'")
|
|
71
|
+
dataset = LOCAL_DATASET_APIS.get(api_name)
|
|
72
|
+
if source == "local":
|
|
73
|
+
if dataset is None:
|
|
74
|
+
raise DatasetError(f"API {api_name!r} does not support local mode")
|
|
75
|
+
return self.local.query(dataset, params, fields=fields)
|
|
76
|
+
if source == "auto" and dataset is not None and self.local.covers(dataset, params):
|
|
77
|
+
return self.local.query(dataset, params, fields=fields)
|
|
78
|
+
return self.transport.query(
|
|
79
|
+
api_name,
|
|
80
|
+
params,
|
|
81
|
+
fields=fields,
|
|
82
|
+
response_format=response_format,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def create_export(
|
|
86
|
+
self,
|
|
87
|
+
api_name: str,
|
|
88
|
+
*,
|
|
89
|
+
fields: list[str] | None = None,
|
|
90
|
+
**params: Any,
|
|
91
|
+
) -> dict[str, Any]:
|
|
92
|
+
"""创建异步 Parquet 导出任务,立即返回任务信息。"""
|
|
93
|
+
return self.transport.create_export(api_name, params, fields=fields)
|
|
94
|
+
|
|
95
|
+
def sql_query(
|
|
96
|
+
self,
|
|
97
|
+
sql: str,
|
|
98
|
+
*,
|
|
99
|
+
bindings: dict[str, str | int | float | bool | None] | None = None,
|
|
100
|
+
max_rows: int = 1_000,
|
|
101
|
+
fields: list[str] | None = None,
|
|
102
|
+
) -> pd.DataFrame:
|
|
103
|
+
"""通过独立权限的服务端接口执行单条受限 SELECT。"""
|
|
104
|
+
return self.query(
|
|
105
|
+
"sql_query",
|
|
106
|
+
sql=sql,
|
|
107
|
+
bindings=bindings or {},
|
|
108
|
+
max_rows=max_rows,
|
|
109
|
+
fields=fields,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def export_status(self, job_id: str) -> dict[str, Any]:
|
|
113
|
+
return self.transport.export_status(job_id)
|
|
114
|
+
|
|
115
|
+
def download_export(self, job_id: str, destination: str | Path) -> Path:
|
|
116
|
+
return self.transport.download_export(job_id, Path(destination))
|
|
117
|
+
|
|
118
|
+
def stock_daily(
|
|
119
|
+
self,
|
|
120
|
+
ts_code: str | list[str] | None = None,
|
|
121
|
+
*,
|
|
122
|
+
trade_date: Any = None,
|
|
123
|
+
start_date: Any = None,
|
|
124
|
+
end_date: Any = None,
|
|
125
|
+
source: Source = "auto",
|
|
126
|
+
fields: list[str] | None = None,
|
|
127
|
+
) -> pd.DataFrame:
|
|
128
|
+
return self.query(
|
|
129
|
+
"stock_daily",
|
|
130
|
+
ts_code=ts_code,
|
|
131
|
+
trade_date=trade_date,
|
|
132
|
+
start_date=start_date,
|
|
133
|
+
end_date=end_date,
|
|
134
|
+
source=source,
|
|
135
|
+
fields=fields,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def stock_adjusting_factor(
|
|
139
|
+
self,
|
|
140
|
+
ts_code: str | list[str] | None = None,
|
|
141
|
+
*,
|
|
142
|
+
trade_date: Any = None,
|
|
143
|
+
start_date: Any = None,
|
|
144
|
+
end_date: Any = None,
|
|
145
|
+
source: Source = "auto",
|
|
146
|
+
fields: list[str] | None = None,
|
|
147
|
+
) -> pd.DataFrame:
|
|
148
|
+
return self.query(
|
|
149
|
+
"stock_adjusting_factor",
|
|
150
|
+
ts_code=ts_code,
|
|
151
|
+
trade_date=trade_date,
|
|
152
|
+
start_date=start_date,
|
|
153
|
+
end_date=end_date,
|
|
154
|
+
source=source,
|
|
155
|
+
fields=fields,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def stock_valuation_daily(
|
|
159
|
+
self,
|
|
160
|
+
ts_code: str | list[str] | None = None,
|
|
161
|
+
*,
|
|
162
|
+
trade_date: Any = None,
|
|
163
|
+
start_date: Any = None,
|
|
164
|
+
end_date: Any = None,
|
|
165
|
+
source: Source = "auto",
|
|
166
|
+
fields: list[str] | None = None,
|
|
167
|
+
) -> pd.DataFrame:
|
|
168
|
+
return self.query(
|
|
169
|
+
"stock_valuation_daily",
|
|
170
|
+
ts_code=ts_code,
|
|
171
|
+
trade_date=trade_date,
|
|
172
|
+
start_date=start_date,
|
|
173
|
+
end_date=end_date,
|
|
174
|
+
source=source,
|
|
175
|
+
fields=fields,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def index_daily(
|
|
179
|
+
self,
|
|
180
|
+
ts_code: str | list[str],
|
|
181
|
+
*,
|
|
182
|
+
trade_date: Any = None,
|
|
183
|
+
start_date: Any = None,
|
|
184
|
+
end_date: Any = None,
|
|
185
|
+
fields: list[str] | None = None,
|
|
186
|
+
) -> pd.DataFrame:
|
|
187
|
+
return self.query(
|
|
188
|
+
"index_daily",
|
|
189
|
+
ts_code=ts_code,
|
|
190
|
+
trade_date=trade_date,
|
|
191
|
+
start_date=start_date,
|
|
192
|
+
end_date=end_date,
|
|
193
|
+
fields=fields,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def index_basic(
|
|
197
|
+
self,
|
|
198
|
+
*,
|
|
199
|
+
ts_code: str | list[str] | None = None,
|
|
200
|
+
symbol: str | list[str] | None = None,
|
|
201
|
+
name: str | None = None,
|
|
202
|
+
market: str | None = None,
|
|
203
|
+
publisher: str | None = None,
|
|
204
|
+
category: str | int | None = None,
|
|
205
|
+
fields: list[str] | None = None,
|
|
206
|
+
) -> pd.DataFrame:
|
|
207
|
+
"""查询指数基本信息。
|
|
208
|
+
|
|
209
|
+
``ts_code`` 可带后缀(如 ``000300.SH``),查询时服务端会剥离后缀按纯代码
|
|
210
|
+
匹配,与 ``symbol`` 行为一致(指数代码在不带后缀时唯一)。返回结果中
|
|
211
|
+
``ts_code`` 为聚源发布机构口径的带后缀代码,并新增 ``inner_code``
|
|
212
|
+
(指数聚源内码)字段。至少需提供一个过滤参数;本接口仅支持远程查询。
|
|
213
|
+
"""
|
|
214
|
+
return self.query(
|
|
215
|
+
"index_basic",
|
|
216
|
+
ts_code=ts_code,
|
|
217
|
+
symbol=symbol,
|
|
218
|
+
name=name,
|
|
219
|
+
market=market,
|
|
220
|
+
publisher=publisher,
|
|
221
|
+
category=category,
|
|
222
|
+
fields=fields,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def index_weight(
|
|
226
|
+
self,
|
|
227
|
+
index_code: str,
|
|
228
|
+
*,
|
|
229
|
+
trade_date: Any = None,
|
|
230
|
+
start_date: Any = None,
|
|
231
|
+
end_date: Any = None,
|
|
232
|
+
fields: list[str] | None = None,
|
|
233
|
+
) -> pd.DataFrame:
|
|
234
|
+
"""查询单个指数的成分证券与权重。
|
|
235
|
+
|
|
236
|
+
``index_code`` 必须带市场后缀,例如 ``399300.SZ``;返回的
|
|
237
|
+
``index_code``、``con_code`` 也始终带后缀。本接口仅支持远程查询。
|
|
238
|
+
"""
|
|
239
|
+
return self.query(
|
|
240
|
+
"index_weight",
|
|
241
|
+
index_code=index_code,
|
|
242
|
+
trade_date=trade_date,
|
|
243
|
+
start_date=start_date,
|
|
244
|
+
end_date=end_date,
|
|
245
|
+
fields=fields,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def macro_indicator_basic(
|
|
249
|
+
self,
|
|
250
|
+
*,
|
|
251
|
+
indicator_code: int | list[int] | None = None,
|
|
252
|
+
name: str | None = None,
|
|
253
|
+
fields: list[str] | None = None,
|
|
254
|
+
) -> pd.DataFrame:
|
|
255
|
+
return self.query(
|
|
256
|
+
"macro_indicator_basic", indicator_code=indicator_code, name=name, fields=fields
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def macro_indicator_data(
|
|
260
|
+
self,
|
|
261
|
+
indicator_code: int | list[int],
|
|
262
|
+
*,
|
|
263
|
+
start_date: Any = None,
|
|
264
|
+
end_date: Any = None,
|
|
265
|
+
fields: list[str] | None = None,
|
|
266
|
+
) -> pd.DataFrame:
|
|
267
|
+
return self.query(
|
|
268
|
+
"macro_indicator_data",
|
|
269
|
+
indicator_code=indicator_code,
|
|
270
|
+
start_date=start_date,
|
|
271
|
+
end_date=end_date,
|
|
272
|
+
fields=fields,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def stock_basic(
|
|
276
|
+
self,
|
|
277
|
+
*,
|
|
278
|
+
ts_code: str | list[str] | None = None,
|
|
279
|
+
name: str | None = None,
|
|
280
|
+
market: str | None = None,
|
|
281
|
+
list_status: str | list[str] | None = "L",
|
|
282
|
+
exchange: str | None = None,
|
|
283
|
+
is_hs: str | None = None,
|
|
284
|
+
fields: list[str] | None = None,
|
|
285
|
+
) -> pd.DataFrame:
|
|
286
|
+
"""查询股票列表基础信息(18 列)。
|
|
287
|
+
|
|
288
|
+
``list_status`` 取 ``L``(上市,默认)/``D``(退市)/``UN``(未上市),
|
|
289
|
+
也可传 ``"all"`` 返回全部;``market`` 取 ``主板``/``创业板``/``科创板``/``北交所``/``CDR``;
|
|
290
|
+
``exchange`` 取 ``SH``/``SZ``/``BJ``(兼容 ``SSE``/``SZSE``/``BSE``);
|
|
291
|
+
``is_hs`` 取 ``N``/``H``(沪股通)/``S``(深股通)。字段口径见
|
|
292
|
+
[股票列表](../../docs/数据接口文档/股票列表.md)。
|
|
293
|
+
"""
|
|
294
|
+
return self.query(
|
|
295
|
+
"stock_basic",
|
|
296
|
+
ts_code=ts_code,
|
|
297
|
+
name=name,
|
|
298
|
+
market=market,
|
|
299
|
+
list_status=list_status,
|
|
300
|
+
exchange=exchange,
|
|
301
|
+
is_hs=is_hs,
|
|
302
|
+
fields=fields,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def trade_cal(
|
|
306
|
+
self,
|
|
307
|
+
*,
|
|
308
|
+
exchange: str = "SH",
|
|
309
|
+
start_date: Any = None,
|
|
310
|
+
end_date: Any = None,
|
|
311
|
+
is_open: str | int | None = None,
|
|
312
|
+
is_week_end: str | int | None = None,
|
|
313
|
+
is_month_end: str | int | None = None,
|
|
314
|
+
is_quarter_end: str | int | None = None,
|
|
315
|
+
is_year_end: str | int | None = None,
|
|
316
|
+
fields: list[str] | None = None,
|
|
317
|
+
) -> pd.DataFrame:
|
|
318
|
+
"""查询单个交易所的交易日历(8 列)。
|
|
319
|
+
|
|
320
|
+
``exchange`` 取 ``SH``/``SZ``/``BJ``/``HK``/``US``/``IB``,默认 ``SH``。
|
|
321
|
+
五个标志均可作为过滤条件,取值 ``'1'``(是)或 ``'0'``(否):
|
|
322
|
+
``is_open``(是否交易日)、``is_week_end``/``is_month_end``/
|
|
323
|
+
``is_quarter_end``/``is_year_end``(是否周/月/季/年最后一个交易日)。
|
|
324
|
+
``US`` 为美国三个市场按 ``cal_date`` 合并后的日历。字段口径见
|
|
325
|
+
[交易日历](../../docs/数据接口文档/交易日历.md)。
|
|
326
|
+
"""
|
|
327
|
+
return self.query(
|
|
328
|
+
"trade_cal",
|
|
329
|
+
exchange=exchange,
|
|
330
|
+
start_date=start_date,
|
|
331
|
+
end_date=end_date,
|
|
332
|
+
is_open=is_open,
|
|
333
|
+
is_week_end=is_week_end,
|
|
334
|
+
is_month_end=is_month_end,
|
|
335
|
+
is_quarter_end=is_quarter_end,
|
|
336
|
+
is_year_end=is_year_end,
|
|
337
|
+
fields=fields,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def etf_adj_daily(
|
|
341
|
+
self,
|
|
342
|
+
*,
|
|
343
|
+
ts_code: str | list[str] | None = None,
|
|
344
|
+
trade_date: Any = None,
|
|
345
|
+
start_date: Any = None,
|
|
346
|
+
end_date: Any = None,
|
|
347
|
+
adjusting_standard: int = 2,
|
|
348
|
+
fields: list[str] | None = None,
|
|
349
|
+
) -> pd.DataFrame:
|
|
350
|
+
"""查询 ETF 后复权行情(13 列)。
|
|
351
|
+
|
|
352
|
+
``adjusting_standard`` 取 ``1`` 精确复权(现金分红口径)或 ``2`` 比例复权
|
|
353
|
+
(分红再投口径,默认);两者价格序列不可混用。本表字典说明"均为后复权",
|
|
354
|
+
故 ``adjusting_method`` 固定为 ``2``,不提供前复权。
|
|
355
|
+
"""
|
|
356
|
+
return self.query(
|
|
357
|
+
"etf_adj_daily",
|
|
358
|
+
ts_code=ts_code,
|
|
359
|
+
trade_date=trade_date,
|
|
360
|
+
start_date=start_date,
|
|
361
|
+
end_date=end_date,
|
|
362
|
+
adjusting_standard=adjusting_standard,
|
|
363
|
+
fields=fields,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def etf_basic(
|
|
367
|
+
self,
|
|
368
|
+
*,
|
|
369
|
+
ts_code: str | list[str] | None = None,
|
|
370
|
+
index_code: str | list[str] | None = None,
|
|
371
|
+
list_date: Any = None,
|
|
372
|
+
list_status: str | None = None,
|
|
373
|
+
exchange: str | None = None,
|
|
374
|
+
mgr: str | None = None,
|
|
375
|
+
fields: list[str] | None = None,
|
|
376
|
+
) -> pd.DataFrame:
|
|
377
|
+
"""查询 ETF 基础信息。
|
|
378
|
+
|
|
379
|
+
参数与 Tushare ``etf_basic`` 对齐:``ts_code`` 可带 ``.SH``/``.SZ`` 后缀
|
|
380
|
+
或纯代码(服务端剥离后缀后匹配,并校验后缀与交易所一致);``index_code``
|
|
381
|
+
是跟踪目标代码,可能是指数,也可能是现货/商品标的;``list_status`` 取
|
|
382
|
+
``L``/``D``/``P``;``exchange`` 取 ``SH``/``SZ``;``mgr`` 按管理人简称
|
|
383
|
+
模糊匹配。返回 16 列,其中 ``inner_code``(ETF 聚源内码)与
|
|
384
|
+
``index_inner_code``(跟踪目标聚源内码)是 ffdata 扩展字段;不传任何过滤
|
|
385
|
+
条件时返回全部沪深上市 ETF。本接口仅支持远程查询。
|
|
386
|
+
"""
|
|
387
|
+
return self.query(
|
|
388
|
+
"etf_basic",
|
|
389
|
+
ts_code=ts_code,
|
|
390
|
+
index_code=index_code,
|
|
391
|
+
list_date=list_date,
|
|
392
|
+
list_status=list_status,
|
|
393
|
+
exchange=exchange,
|
|
394
|
+
mgr=mgr,
|
|
395
|
+
fields=fields,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def etf_daily(
|
|
399
|
+
self,
|
|
400
|
+
ts_code: str | list[str] | None = None,
|
|
401
|
+
*,
|
|
402
|
+
trade_date: Any = None,
|
|
403
|
+
start_date: Any = None,
|
|
404
|
+
end_date: Any = None,
|
|
405
|
+
fields: list[str] | None = None,
|
|
406
|
+
) -> pd.DataFrame:
|
|
407
|
+
"""查询 ETF 日线行情。
|
|
408
|
+
|
|
409
|
+
``ts_code`` 可带 ``.SH``/``.SZ`` 后缀或纯代码,支持列表与逗号分隔;至少要传
|
|
410
|
+
``ts_code`` 或 ``trade_date`` 之一,``trade_date`` 与日期区间互斥,只给区间而
|
|
411
|
+
不给代码会被拒绝。返回 83 列:Tushare ``fund_daily`` 的 11 列兼容段 +
|
|
412
|
+
``inner_code`` + 日表现扩展列 + 近一周/近一月/近一年区间表现列;``vol`` 与区间
|
|
413
|
+
成交量为**份**,``amount`` 为**元**。本接口仅支持远程查询。
|
|
414
|
+
"""
|
|
415
|
+
return self.query(
|
|
416
|
+
"etf_daily",
|
|
417
|
+
ts_code=ts_code,
|
|
418
|
+
trade_date=trade_date,
|
|
419
|
+
start_date=start_date,
|
|
420
|
+
end_date=end_date,
|
|
421
|
+
fields=fields,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
def fund_basic(
|
|
425
|
+
self,
|
|
426
|
+
*,
|
|
427
|
+
ts_code: str | list[str] | None = None,
|
|
428
|
+
market: str | None = None,
|
|
429
|
+
status: str | None = None,
|
|
430
|
+
fields: list[str] | None = None,
|
|
431
|
+
) -> pd.DataFrame:
|
|
432
|
+
"""查询公募基金列表(71 列,对齐 Tushare ``fund_basic`` 并输出聚源全字段)。
|
|
433
|
+
|
|
434
|
+
``ts_code`` 可带 ``.SH``/``.SZ``(场内)或 ``.OF``(场外)后缀;``market`` 取
|
|
435
|
+
``E`` 场内 / ``O`` 场外,**默认返回场内 + 场外全部**(与 Tushare 默认 ``E`` 不同);
|
|
436
|
+
``status`` 取 ``D`` 已到期终止 / ``I`` 发行中 / ``L`` 已设立存续。
|
|
437
|
+
聚源主表无来源的 9 个 Tushare 列(``issue_date``/``delist_date``/``m_fee``/``c_fee``/
|
|
438
|
+
``p_value``/``exp_return``/``trustee``/``purc_startdate``/``redm_startdate``)不在本接口输出。
|
|
439
|
+
"""
|
|
440
|
+
return self.query(
|
|
441
|
+
"fund_basic",
|
|
442
|
+
ts_code=ts_code,
|
|
443
|
+
market=market,
|
|
444
|
+
status=status,
|
|
445
|
+
fields=fields,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
def cb_daily(
|
|
449
|
+
self,
|
|
450
|
+
*,
|
|
451
|
+
ts_code: str | list[str] | None = None,
|
|
452
|
+
trade_date: Any = None,
|
|
453
|
+
start_date: Any = None,
|
|
454
|
+
end_date: Any = None,
|
|
455
|
+
fields: list[str] | None = None,
|
|
456
|
+
) -> pd.DataFrame:
|
|
457
|
+
"""查询可转债行情(22 列,对齐 Tushare ``cb_daily`` 的前 11 列 + 聚源扩展)。
|
|
458
|
+
|
|
459
|
+
``trade_date`` 与 ``start_date``/``end_date`` 互斥,且至少需要 ``ts_code`` 或
|
|
460
|
+
``trade_date`` 之一。`vol` 与聚源 ``TurnoverVolume`` 同口径(不做单位换算),
|
|
461
|
+
`amount` 为元。Tushare 的纯债价值/溢价率与转股价值/溢价率 4 列不在本接口,
|
|
462
|
+
请调用 :meth:`cb_deriv`(可按 ``ts_code + trade_date`` 合并)。
|
|
463
|
+
"""
|
|
464
|
+
return self.query(
|
|
465
|
+
"cb_daily",
|
|
466
|
+
ts_code=ts_code,
|
|
467
|
+
trade_date=trade_date,
|
|
468
|
+
start_date=start_date,
|
|
469
|
+
end_date=end_date,
|
|
470
|
+
fields=fields,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def cb_deriv(
|
|
474
|
+
self,
|
|
475
|
+
*,
|
|
476
|
+
ts_code: str | list[str] | None = None,
|
|
477
|
+
trade_date: Any = None,
|
|
478
|
+
start_date: Any = None,
|
|
479
|
+
end_date: Any = None,
|
|
480
|
+
exchange: str | None = None,
|
|
481
|
+
fields: list[str] | None = None,
|
|
482
|
+
) -> pd.DataFrame:
|
|
483
|
+
"""查询可转债衍生指标(26 列,ffdata 扩展接口)。
|
|
484
|
+
|
|
485
|
+
提供 Tushare ``cb_daily`` 对应关系:``cb_strb_value``(纯债价值)、
|
|
486
|
+
``cb_strb_premium_ratio``(纯债溢价率)、``cb_convert_value``(转股价值)、
|
|
487
|
+
``convert_premium_rate``(转股溢价率),以及 YTM、久期、凸性、双低等指标。
|
|
488
|
+
过滤规则与 :meth:`cb_daily` 相同,另支持 ``exchange``(``SH``/``SZ``)。
|
|
489
|
+
"""
|
|
490
|
+
return self.query(
|
|
491
|
+
"cb_deriv",
|
|
492
|
+
ts_code=ts_code,
|
|
493
|
+
trade_date=trade_date,
|
|
494
|
+
start_date=start_date,
|
|
495
|
+
end_date=end_date,
|
|
496
|
+
exchange=exchange,
|
|
497
|
+
fields=fields,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
def cb_issue(
|
|
501
|
+
self,
|
|
502
|
+
*,
|
|
503
|
+
ts_code: str | list[str] | None = None,
|
|
504
|
+
ann_date: Any = None,
|
|
505
|
+
start_date: Any = None,
|
|
506
|
+
end_date: Any = None,
|
|
507
|
+
fields: list[str] | None = None,
|
|
508
|
+
) -> pd.DataFrame:
|
|
509
|
+
"""查询可转债发行信息(90 列,对齐 Tushare ``cb_issue`` + 聚源全字段)。
|
|
510
|
+
|
|
511
|
+
``ann_date`` 为发行公告日(服务端按聚源募集说明书发布日比较),与日期区间互斥;
|
|
512
|
+
不传任何过滤条件时返回全部历史可转债发行记录。兼容段一律为聚源原值:
|
|
513
|
+
``onl_size``/``shd_ration_size``/``offl_size`` 单位为**元**(Tushare 为张,
|
|
514
|
+
可按 ``par_value`` 换算);``shd_ration_price`` 聚源无来源,本接口不输出该列。
|
|
515
|
+
"""
|
|
516
|
+
return self.query(
|
|
517
|
+
"cb_issue",
|
|
518
|
+
ts_code=ts_code,
|
|
519
|
+
ann_date=ann_date,
|
|
520
|
+
start_date=start_date,
|
|
521
|
+
end_date=end_date,
|
|
522
|
+
fields=fields,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
def cb_basic(
|
|
526
|
+
self,
|
|
527
|
+
*,
|
|
528
|
+
ts_code: str | list[str] | None = None,
|
|
529
|
+
list_date: Any = None,
|
|
530
|
+
exchange: str | None = None,
|
|
531
|
+
latest_status: int | None = None,
|
|
532
|
+
fields: list[str] | None = None,
|
|
533
|
+
) -> pd.DataFrame:
|
|
534
|
+
"""查询可转债基本信息。
|
|
535
|
+
|
|
536
|
+
参数与 Tushare ``cb_basic`` 对齐,并额外支持 ``latest_status``:``ts_code`` 可带
|
|
537
|
+
``.SH``/``.SZ`` 后缀或纯代码,支持列表与逗号分隔(服务端剥离后缀匹配并校验市场一致);
|
|
538
|
+
``list_date`` 为上市日期(精确到日);``exchange`` 取 ``SH``/``SZ``;
|
|
539
|
+
``latest_status`` 为聚源最新状态(``1`` 发行中、``2`` 存续中、``3`` 已到期、
|
|
540
|
+
``4`` 发行失败)。返回 125 列:38 列 Tushare 兼容段 + 74 列聚源主表扩展 +
|
|
541
|
+
13 列关联扩展(含条款文本、最新评级、剩余金额等)。不传过滤条件时返回全部沪深
|
|
542
|
+
可转债;本接口仅支持远程查询。
|
|
543
|
+
"""
|
|
544
|
+
return self.query(
|
|
545
|
+
"cb_basic",
|
|
546
|
+
ts_code=ts_code,
|
|
547
|
+
list_date=list_date,
|
|
548
|
+
exchange=exchange,
|
|
549
|
+
latest_status=latest_status,
|
|
550
|
+
fields=fields,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
__all__ = ["Client", "Source"]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""客户端本地数据集的事务式同步。
|
|
2
|
+
|
|
3
|
+
同步采用 manifest 差异比较、断点续传、文件大小与 SHA-256 校验,并在全部分片
|
|
4
|
+
成功后原子替换本地 manifest,避免查询到半更新状态的数据集。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ffdata_common.errors import DatasetError
|
|
14
|
+
|
|
15
|
+
from ..local import LocalStore
|
|
16
|
+
from ..transport import HttpTransport
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _sha256(path: Path) -> str:
|
|
20
|
+
digest = hashlib.sha256()
|
|
21
|
+
with path.open("rb") as source:
|
|
22
|
+
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
|
23
|
+
digest.update(chunk)
|
|
24
|
+
return digest.hexdigest()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DatasetManager:
|
|
28
|
+
"""管理远程数据集发现、本地状态检查、同步和完整性校验。"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, transport: HttpTransport, local_store: LocalStore):
|
|
31
|
+
self.transport = transport
|
|
32
|
+
self.local_store = local_store
|
|
33
|
+
|
|
34
|
+
def list(self) -> list[str]:
|
|
35
|
+
return self.transport.dataset_names()
|
|
36
|
+
|
|
37
|
+
def status(self, name: str) -> dict[str, Any]:
|
|
38
|
+
local = self.local_store.manifest(name)
|
|
39
|
+
remote = self.transport.dataset_manifest(name)
|
|
40
|
+
complete = bool(local) and all(
|
|
41
|
+
(self.local_store.root / name / item.path).is_file() for item in local.files
|
|
42
|
+
)
|
|
43
|
+
return {
|
|
44
|
+
"dataset": name,
|
|
45
|
+
"installed": local is not None,
|
|
46
|
+
"complete": complete,
|
|
47
|
+
"local_version": local.version if local else None,
|
|
48
|
+
"remote_version": remote.version,
|
|
49
|
+
"update_available": local is None or local.version != remote.version,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def sync(self, name: str) -> dict[str, Any]:
|
|
53
|
+
"""增量同步指定数据集,返回下载文件数和最终版本。"""
|
|
54
|
+
remote = self.transport.dataset_manifest(name)
|
|
55
|
+
local = self.local_store.manifest(name)
|
|
56
|
+
local_files = {item.path: item for item in local.files} if local else {}
|
|
57
|
+
dataset_root = self.local_store.root / name
|
|
58
|
+
dataset_root.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
pending = []
|
|
61
|
+
for item in remote.files:
|
|
62
|
+
target = dataset_root / item.path
|
|
63
|
+
local_item = local_files.get(item.path)
|
|
64
|
+
if target.is_file() and target.stat().st_size == item.size:
|
|
65
|
+
if local_item is not None and local_item.sha256 == item.sha256:
|
|
66
|
+
continue
|
|
67
|
+
if _sha256(target) == item.sha256:
|
|
68
|
+
continue
|
|
69
|
+
pending.append(item.path)
|
|
70
|
+
|
|
71
|
+
if pending:
|
|
72
|
+
plan = self.transport.download_plan(name, pending)
|
|
73
|
+
planned = {item.path: item for item in plan.files}
|
|
74
|
+
if set(planned) != set(pending):
|
|
75
|
+
raise DatasetError("download plan does not match the requested files")
|
|
76
|
+
for manifest_file in remote.files:
|
|
77
|
+
if manifest_file.path not in planned:
|
|
78
|
+
continue
|
|
79
|
+
target = dataset_root / manifest_file.path
|
|
80
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
partial = target.with_suffix(f"{target.suffix}.partial")
|
|
82
|
+
offset = partial.stat().st_size if partial.is_file() else 0
|
|
83
|
+
if offset > manifest_file.size:
|
|
84
|
+
partial.unlink()
|
|
85
|
+
offset = 0
|
|
86
|
+
self.transport.download(
|
|
87
|
+
planned[manifest_file.path].url,
|
|
88
|
+
partial,
|
|
89
|
+
offset=offset,
|
|
90
|
+
)
|
|
91
|
+
if partial.stat().st_size != manifest_file.size:
|
|
92
|
+
raise DatasetError(f"downloaded size mismatch for {manifest_file.path}")
|
|
93
|
+
if _sha256(partial) != manifest_file.sha256:
|
|
94
|
+
raise DatasetError(f"checksum mismatch for {manifest_file.path}")
|
|
95
|
+
partial.replace(target)
|
|
96
|
+
|
|
97
|
+
manifest_path = dataset_root / "manifest.json"
|
|
98
|
+
temporary = manifest_path.with_suffix(".json.partial")
|
|
99
|
+
temporary.write_text(remote.model_dump_json(indent=2), encoding="utf-8")
|
|
100
|
+
temporary.replace(manifest_path)
|
|
101
|
+
return {
|
|
102
|
+
"dataset": name,
|
|
103
|
+
"version": remote.version,
|
|
104
|
+
"downloaded_files": len(pending),
|
|
105
|
+
"total_files": len(remote.files),
|
|
106
|
+
"path": str(dataset_root),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def verify(self, name: str) -> dict[str, Any]:
|
|
110
|
+
"""重新计算本地全部分片的 SHA-256,报告损坏或缺失文件。"""
|
|
111
|
+
manifest = self.local_store.manifest(name)
|
|
112
|
+
if manifest is None:
|
|
113
|
+
raise DatasetError(f"local dataset {name!r} is not installed")
|
|
114
|
+
invalid: list[str] = []
|
|
115
|
+
for item in manifest.files:
|
|
116
|
+
path = self.local_store.root / name / item.path
|
|
117
|
+
if (
|
|
118
|
+
not path.is_file()
|
|
119
|
+
or path.stat().st_size != item.size
|
|
120
|
+
or _sha256(path) != item.sha256
|
|
121
|
+
):
|
|
122
|
+
invalid.append(item.path)
|
|
123
|
+
return {
|
|
124
|
+
"dataset": name,
|
|
125
|
+
"version": manifest.version,
|
|
126
|
+
"valid": not invalid,
|
|
127
|
+
"invalid_files": invalid,
|
|
128
|
+
"total_files": len(manifest.files),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__ = ["DatasetManager"]
|