tdxquant 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.
- tdx_api/__init__.py +57 -0
- tdx_api/api/__init__.py +16 -0
- tdx_api/api/calendar.py +53 -0
- tdx_api/api/financial.py +206 -0
- tdx_api/api/instrument.py +60 -0
- tdx_api/api/market.py +168 -0
- tdx_api/api/sector.py +130 -0
- tdx_api/client.py +186 -0
- tdx_api/enums.py +124 -0
- tdx_api/exceptions.py +16 -0
- tdx_api/schema.py +168 -0
- tdxquant-0.1.0.dist-info/METADATA +117 -0
- tdxquant-0.1.0.dist-info/RECORD +14 -0
- tdxquant-0.1.0.dist-info/WHEEL +4 -0
tdx_api/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""通达信量化(TdxQuant)HTTP SDK。
|
|
3
|
+
|
|
4
|
+
直接调用通达信客户端官方 HTTP 接口(``POST http://127.0.0.1:17709/``),
|
|
5
|
+
统一返回 ``pandas.DataFrame``,字段已重命名为金融行业 snake_case 规范。
|
|
6
|
+
|
|
7
|
+
快速上手::
|
|
8
|
+
|
|
9
|
+
from tdx_api import TdxAPI
|
|
10
|
+
|
|
11
|
+
api = TdxAPI() # 默认 http://127.0.0.1:17709,需通达信客户端在线
|
|
12
|
+
df = api.get_kline("000001.SZ", freq="daily", adjust="qfq", start_date="20260101")
|
|
13
|
+
snap = api.get_snapshot("000001.SZ")
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from .client import DEFAULT_BASE_URL, _ClientBase
|
|
18
|
+
from .enums import Adjust, Freq, Market
|
|
19
|
+
from .exceptions import TdxQuantConnectionError, TdxQuantError
|
|
20
|
+
from .api.calendar import CalendarMixin
|
|
21
|
+
from .api.financial import FinancialMixin
|
|
22
|
+
from .api.instrument import InstrumentMixin
|
|
23
|
+
from .api.market import MarketMixin
|
|
24
|
+
from .api.sector import SectorMixin
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"TdxAPI",
|
|
30
|
+
"TdxQuantError",
|
|
31
|
+
"TdxQuantConnectionError",
|
|
32
|
+
"Freq",
|
|
33
|
+
"Adjust",
|
|
34
|
+
"Market",
|
|
35
|
+
"DEFAULT_BASE_URL",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TdxAPI(
|
|
40
|
+
MarketMixin,
|
|
41
|
+
FinancialMixin,
|
|
42
|
+
SectorMixin,
|
|
43
|
+
InstrumentMixin,
|
|
44
|
+
CalendarMixin,
|
|
45
|
+
_ClientBase,
|
|
46
|
+
):
|
|
47
|
+
"""通达信量化 HTTP SDK 客户端。
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
base_url: 通达信客户端 HTTP 地址,默认 ``http://127.0.0.1:17709``
|
|
51
|
+
(需先启动支持 TQ 的通达信客户端)。
|
|
52
|
+
timeout: HTTP 超时秒数。
|
|
53
|
+
|
|
54
|
+
所有数据接口统一返回 ``pandas.DataFrame``;缓存/下载类操作返回 dict。
|
|
55
|
+
字段命名遵循 snake_case 金融行业惯例,原字段血缘见
|
|
56
|
+
``docs/field_lineage.md``。
|
|
57
|
+
"""
|
tdx_api/api/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""客户端业务接口模块(mixin)。
|
|
3
|
+
|
|
4
|
+
各模块以 mixin 形式实现,最终由 :class:`tdx_api.TdxAPI` 组合继承,
|
|
5
|
+
共享 :class:`tdx_api.client._ClientBase` 提供的 HTTP 与 DataFrame 工具。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Iterable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _as_list(x) -> list:
|
|
13
|
+
"""把单个 str 或可迭代对象统一成 list。"""
|
|
14
|
+
if isinstance(x, str):
|
|
15
|
+
return [x]
|
|
16
|
+
return list(x)
|
tdx_api/api/calendar.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""交易日历与缓存/下载接口。"""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from ..enums import validate_date
|
|
8
|
+
from . import _as_list
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CalendarMixin:
|
|
12
|
+
"""交易日历、缓存刷新与文件下载接口。"""
|
|
13
|
+
|
|
14
|
+
def get_trading_calendar(
|
|
15
|
+
self, start_date: str, end_date: str, market: str = "SH"
|
|
16
|
+
) -> pd.DataFrame:
|
|
17
|
+
"""获取交易日历。
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
start_date: 起始日期 ``YYYYMMDD``。
|
|
21
|
+
end_date: 结束日期 ``YYYYMMDD``。
|
|
22
|
+
market: 市场,默认 ``SH``(上交所)。
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
单列 DataFrame,列名 ``date``。
|
|
26
|
+
"""
|
|
27
|
+
start_date = validate_date(start_date, "start_date")
|
|
28
|
+
end_date = validate_date(end_date, "end_date")
|
|
29
|
+
result = self._call(
|
|
30
|
+
"get_trading_calendar", market=market,
|
|
31
|
+
start_time=start_date, end_time=end_date,
|
|
32
|
+
)
|
|
33
|
+
dates = result.get("Date", []) or []
|
|
34
|
+
return pd.DataFrame({"date": dates})
|
|
35
|
+
|
|
36
|
+
def refresh_cache(self, market: str = "AG", force: bool = False) -> dict:
|
|
37
|
+
"""刷新行情缓存(刷新后 5 分钟内取数据不触发刷新)。"""
|
|
38
|
+
return self._call("refresh_cache", market=market, force=force)
|
|
39
|
+
|
|
40
|
+
def refresh_kline(self, stock_list, period: str = "") -> dict:
|
|
41
|
+
"""缓存历史 K线(仅支持 1m/5m/1d,不宜一次太多)。"""
|
|
42
|
+
return self._call(
|
|
43
|
+
"refresh_kline", stock_list=_as_list(stock_list), period=period or None
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def download_file(
|
|
47
|
+
self, code: str, down_time: str = "", down_type: int = 1
|
|
48
|
+
) -> dict:
|
|
49
|
+
"""下载十大股东(``down_type=1``)/ ETF 申赎清单(``down_type=2``)。"""
|
|
50
|
+
return self._call(
|
|
51
|
+
"download_file", stock_code=code,
|
|
52
|
+
down_time=down_time or None, down_type=down_type,
|
|
53
|
+
)
|
tdx_api/api/financial.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""财务类接口:专业财务、股票/板块/市场交易数据、单点数据、股本数据。
|
|
3
|
+
|
|
4
|
+
说明:
|
|
5
|
+
- 专业财务字段(``FNxxx``)、交易数据字段(``GPx``/``BKx``/``SCx``/``GOx``)为通达信
|
|
6
|
+
内部编码,SDK 保留编码转小写(``FN193`` → ``fn193``);时间字段 ``announce_time`` /
|
|
7
|
+
``tag_time`` 保留。
|
|
8
|
+
- 底层 HTTP 接口的字段参数名为 ``table_list``(非 tqcenter 的 ``field_list``)。
|
|
9
|
+
- ``get_financial`` 的 ``end_time`` 为必填,留空时自动取当天。
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
from ..enums import validate_date
|
|
18
|
+
from . import _as_list
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _date_to_year_mmdd(date_str: str) -> tuple[int, int]:
|
|
22
|
+
"""``YYYYMMDD`` → ``(year, mmdd)``,空串返回 ``(0, 0)``。"""
|
|
23
|
+
if not date_str:
|
|
24
|
+
return 0, 0
|
|
25
|
+
return int(date_str[:4]), int(date_str[4:8])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FinancialMixin:
|
|
29
|
+
"""财务与交易数据接口。"""
|
|
30
|
+
|
|
31
|
+
def get_financial(
|
|
32
|
+
self, stock_list, fields=None, start_date="", end_date="",
|
|
33
|
+
report_type="report_time",
|
|
34
|
+
) -> pd.DataFrame:
|
|
35
|
+
"""专业财务数据(需客户端下载专业财务数据)。
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
stock_list: 证券代码或列表。
|
|
39
|
+
fields: 字段列表(通达信编码,如 ``["Fn193", "Fn194"]``)。
|
|
40
|
+
start_date / end_date: 日期区间 ``YYYYMMDD``(end 留空取当天)。
|
|
41
|
+
report_type: ``report_time`` 按截止日期 / ``announce_time`` 按公告日期。
|
|
42
|
+
"""
|
|
43
|
+
stock_list = _as_list(stock_list)
|
|
44
|
+
start_date = validate_date(start_date, "start_date")
|
|
45
|
+
end_date = validate_date(end_date, "end_date") or datetime.now().strftime("%Y%m%d")
|
|
46
|
+
result = self._call(
|
|
47
|
+
"get_financial_data",
|
|
48
|
+
stock_list=stock_list,
|
|
49
|
+
table_list=fields or [],
|
|
50
|
+
start_time=start_date or None,
|
|
51
|
+
end_time=end_date,
|
|
52
|
+
report_type=report_type,
|
|
53
|
+
)
|
|
54
|
+
return self._stock_value_to_df(result, stock_list)
|
|
55
|
+
|
|
56
|
+
def get_financial_by_date(
|
|
57
|
+
self, stock_list, fields=None, date=""
|
|
58
|
+
) -> pd.DataFrame:
|
|
59
|
+
"""指定日期的专业财务数据。"""
|
|
60
|
+
stock_list = _as_list(stock_list)
|
|
61
|
+
year, mmdd = _date_to_year_mmdd(validate_date(date, "date"))
|
|
62
|
+
result = self._call(
|
|
63
|
+
"get_financial_data_by_date",
|
|
64
|
+
stock_list=stock_list, table_list=fields or [], year=year, mmdd=mmdd,
|
|
65
|
+
)
|
|
66
|
+
return self._stock_value_to_df(result, stock_list)
|
|
67
|
+
|
|
68
|
+
def get_stock_trade_data(
|
|
69
|
+
self, stock_list, fields=None, start_date="", end_date=""
|
|
70
|
+
) -> pd.DataFrame:
|
|
71
|
+
"""股票交易数据(字段 GPx)。"""
|
|
72
|
+
stock_list = _as_list(stock_list)
|
|
73
|
+
start_date = validate_date(start_date, "start_date")
|
|
74
|
+
end_date = validate_date(end_date, "end_date")
|
|
75
|
+
result = self._call(
|
|
76
|
+
"get_gpjy_value",
|
|
77
|
+
stock_list=stock_list, table_list=fields or [],
|
|
78
|
+
start_time=start_date or None, end_time=end_date or None,
|
|
79
|
+
)
|
|
80
|
+
return self._stock_value_to_df(result, stock_list)
|
|
81
|
+
|
|
82
|
+
def get_stock_trade_data_by_date(
|
|
83
|
+
self, stock_list, fields=None, date=""
|
|
84
|
+
) -> pd.DataFrame:
|
|
85
|
+
"""指定日期的股票交易数据。"""
|
|
86
|
+
stock_list = _as_list(stock_list)
|
|
87
|
+
year, mmdd = _date_to_year_mmdd(validate_date(date, "date"))
|
|
88
|
+
result = self._call(
|
|
89
|
+
"get_gpjy_value_by_date",
|
|
90
|
+
stock_list=stock_list, table_list=fields or [], year=year, mmdd=mmdd,
|
|
91
|
+
)
|
|
92
|
+
return self._stock_value_to_df(result, stock_list)
|
|
93
|
+
|
|
94
|
+
def get_sector_trade_data(
|
|
95
|
+
self, stock_list, fields=None, start_date="", end_date=""
|
|
96
|
+
) -> pd.DataFrame:
|
|
97
|
+
"""板块交易数据(字段 BKx)。"""
|
|
98
|
+
stock_list = _as_list(stock_list)
|
|
99
|
+
start_date = validate_date(start_date, "start_date")
|
|
100
|
+
end_date = validate_date(end_date, "end_date")
|
|
101
|
+
result = self._call(
|
|
102
|
+
"get_bkjy_value",
|
|
103
|
+
stock_list=stock_list, table_list=fields or [],
|
|
104
|
+
start_time=start_date or None, end_time=end_date or None,
|
|
105
|
+
)
|
|
106
|
+
return self._stock_value_to_df(result, stock_list)
|
|
107
|
+
|
|
108
|
+
def get_sector_trade_data_by_date(
|
|
109
|
+
self, stock_list, fields=None, date=""
|
|
110
|
+
) -> pd.DataFrame:
|
|
111
|
+
"""指定日期的板块交易数据。"""
|
|
112
|
+
stock_list = _as_list(stock_list)
|
|
113
|
+
year, mmdd = _date_to_year_mmdd(validate_date(date, "date"))
|
|
114
|
+
result = self._call(
|
|
115
|
+
"get_bkjy_value_by_date",
|
|
116
|
+
stock_list=stock_list, table_list=fields or [], year=year, mmdd=mmdd,
|
|
117
|
+
)
|
|
118
|
+
return self._stock_value_to_df(result, stock_list)
|
|
119
|
+
|
|
120
|
+
def get_market_trade_data(
|
|
121
|
+
self, fields=None, start_date="", end_date=""
|
|
122
|
+
) -> pd.DataFrame:
|
|
123
|
+
"""市场交易数据(字段 SCx,无个股维度)。"""
|
|
124
|
+
start_date = validate_date(start_date, "start_date")
|
|
125
|
+
end_date = validate_date(end_date, "end_date")
|
|
126
|
+
result = self._call(
|
|
127
|
+
"get_scjy_value",
|
|
128
|
+
table_list=fields or [],
|
|
129
|
+
start_time=start_date or None, end_time=end_date or None,
|
|
130
|
+
)
|
|
131
|
+
return self._flat_value_to_df(result)
|
|
132
|
+
|
|
133
|
+
def get_market_trade_data_by_date(
|
|
134
|
+
self, fields=None, date=""
|
|
135
|
+
) -> pd.DataFrame:
|
|
136
|
+
"""指定日期的市场交易数据。"""
|
|
137
|
+
year, mmdd = _date_to_year_mmdd(validate_date(date, "date"))
|
|
138
|
+
result = self._call(
|
|
139
|
+
"get_scjy_value_by_date",
|
|
140
|
+
table_list=fields or [], year=year, mmdd=mmdd,
|
|
141
|
+
)
|
|
142
|
+
return self._flat_value_to_df(result)
|
|
143
|
+
|
|
144
|
+
def get_stock_single_data(self, stock_list, fields=None) -> pd.DataFrame:
|
|
145
|
+
"""股票单个数据(非序列,字段 GOx)。"""
|
|
146
|
+
stock_list = _as_list(stock_list)
|
|
147
|
+
result = self._call(
|
|
148
|
+
"get_gp_one_data", stock_list=stock_list, table_list=fields or [],
|
|
149
|
+
)
|
|
150
|
+
return self._stock_value_to_df(result, stock_list)
|
|
151
|
+
|
|
152
|
+
def get_share_capital(self, code, date_list=None, count=1) -> pd.DataFrame:
|
|
153
|
+
"""每日股本数据(总股本/流通股本等)。"""
|
|
154
|
+
result = self._call(
|
|
155
|
+
"get_gb_info", stock_code=code, date_list=date_list or [], count=count,
|
|
156
|
+
)
|
|
157
|
+
return self._flat_value_to_df(result)
|
|
158
|
+
|
|
159
|
+
def get_share_capital_by_date(
|
|
160
|
+
self, code, start_date="", end_date=""
|
|
161
|
+
) -> pd.DataFrame:
|
|
162
|
+
"""按日期段获取股本数据。"""
|
|
163
|
+
start_date = validate_date(start_date, "start_date")
|
|
164
|
+
end_date = validate_date(end_date, "end_date")
|
|
165
|
+
result = self._call(
|
|
166
|
+
"get_gb_info_by_date",
|
|
167
|
+
stock_code=code,
|
|
168
|
+
start_date=start_date or None, end_date=end_date or None,
|
|
169
|
+
)
|
|
170
|
+
return self._flat_value_to_df(result)
|
|
171
|
+
|
|
172
|
+
# ---------------- 解析工具 ----------------
|
|
173
|
+
def _stock_value_to_df(self, result, stock_list) -> pd.DataFrame:
|
|
174
|
+
"""``result.Value.{stock: {field: [list]|value}}`` → 长表,加 code 列。"""
|
|
175
|
+
value = result.get("Value", {})
|
|
176
|
+
parts: list[pd.DataFrame] = []
|
|
177
|
+
items = value.items() if isinstance(value, dict) else []
|
|
178
|
+
for stock, sv in items:
|
|
179
|
+
if stock not in stock_list or sv is None:
|
|
180
|
+
continue
|
|
181
|
+
if isinstance(sv, dict):
|
|
182
|
+
sdf = pd.DataFrame(sv)
|
|
183
|
+
else:
|
|
184
|
+
sdf = pd.DataFrame([{"value": sv}])
|
|
185
|
+
sdf.insert(0, "code", stock)
|
|
186
|
+
parts.append(sdf.reset_index(drop=True))
|
|
187
|
+
if not parts:
|
|
188
|
+
return pd.DataFrame()
|
|
189
|
+
out = pd.concat(parts, ignore_index=True)
|
|
190
|
+
out.columns = [c.lower() if c != "code" else c for c in out.columns]
|
|
191
|
+
return out
|
|
192
|
+
|
|
193
|
+
def _flat_value_to_df(self, result) -> pd.DataFrame:
|
|
194
|
+
"""``result.Value`` 为扁平 dict / ``{field: [list]}`` / list → DataFrame。"""
|
|
195
|
+
value = result.get("Value", result)
|
|
196
|
+
if value is None or value == "":
|
|
197
|
+
return pd.DataFrame()
|
|
198
|
+
if isinstance(value, dict):
|
|
199
|
+
df = pd.DataFrame(value)
|
|
200
|
+
elif isinstance(value, list):
|
|
201
|
+
df = self._list_to_df(value)
|
|
202
|
+
else:
|
|
203
|
+
return pd.DataFrame()
|
|
204
|
+
if not df.empty:
|
|
205
|
+
df.columns = [str(c).lower() for c in df.columns]
|
|
206
|
+
return df
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""证券类接口:可转债、新股/新债申购、跟踪指数的 ETF。"""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from ..schema import CB_FIELD_MAP, ETF_FIELD_MAP, IPO_FIELD_MAP
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InstrumentMixin:
|
|
11
|
+
"""可转债 / 新股 / ETF 接口。"""
|
|
12
|
+
|
|
13
|
+
def get_convertible_bond(self, code: str, fields=None) -> pd.DataFrame:
|
|
14
|
+
"""获取可转债基础信息(HTTP 返回 Value 为 list,取首项)。
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
单行 DataFrame,列见 ``docs/field_lineage.md`` 可转债表。
|
|
18
|
+
"""
|
|
19
|
+
result = self._call("get_kzz_info", stock_code=code, field_list=fields or None)
|
|
20
|
+
value = result.get("Value", [])
|
|
21
|
+
if not value:
|
|
22
|
+
return pd.DataFrame()
|
|
23
|
+
item = value[0] if isinstance(value, list) else value
|
|
24
|
+
row = self._dict_to_row(item, CB_FIELD_MAP)
|
|
25
|
+
return self._rows_to_df([row])
|
|
26
|
+
|
|
27
|
+
def get_ipo_info(self, ipo_type: int = 0, ipo_date: int = 0) -> pd.DataFrame:
|
|
28
|
+
"""获取新股 / 新债申购信息。
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
ipo_type: ``0`` 全部 / ``1`` 新债 / ``2`` 新股。
|
|
32
|
+
ipo_date: ``0`` 今天 / ``1`` 今天及以后。
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
DataFrame,列:``code, name, subscribe_date, subscribe_price,
|
|
36
|
+
subscribe_code, max_subscribe, issue_pe``。
|
|
37
|
+
"""
|
|
38
|
+
result = self._call("get_ipo_info", ipo_type=ipo_type, ipo_date=ipo_date)
|
|
39
|
+
value = result.get("Value", []) or []
|
|
40
|
+
df = self._list_to_df(value)
|
|
41
|
+
if not df.empty:
|
|
42
|
+
df = df.rename(columns=lambda c: self._rename(IPO_FIELD_MAP, c))
|
|
43
|
+
return df
|
|
44
|
+
|
|
45
|
+
def get_track_etf(self, zs_code: str) -> pd.DataFrame:
|
|
46
|
+
"""获取跟踪指定指数的 ETF 列表。
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
zs_code: 指数代码,如 ``000300.SH``。
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
DataFrame,列:``code, name, last_price, pre_close, iopv,
|
|
53
|
+
total_share, scale``。
|
|
54
|
+
"""
|
|
55
|
+
result = self._call("get_trackzs_etf_info", zs_code=zs_code)
|
|
56
|
+
value = result.get("Value", []) or []
|
|
57
|
+
df = self._list_to_df(value)
|
|
58
|
+
if not df.empty:
|
|
59
|
+
df = df.rename(columns=lambda c: self._rename(ETF_FIELD_MAP, c))
|
|
60
|
+
return df
|
tdx_api/api/market.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""行情类接口:K线、快照、基本信息、更多信息、分红送配。
|
|
3
|
+
|
|
4
|
+
直接调用通达信官方 HTTP 接口(POST 127.0.0.1:17709),字段血缘见
|
|
5
|
+
:mod:`tdx_api.schema` 与 ``docs/field_lineage.md``。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from ..enums import adjust_to_dividend, freq_to_period, validate_date
|
|
14
|
+
from ..schema import (
|
|
15
|
+
DIVIDEND_FIELD_MAP,
|
|
16
|
+
SNAPSHOT_FIELD_MAP,
|
|
17
|
+
STOCK_INFO_FIELD_MAP,
|
|
18
|
+
)
|
|
19
|
+
from . import _as_list
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MarketMixin:
|
|
23
|
+
"""行情数据接口。"""
|
|
24
|
+
|
|
25
|
+
def get_kline(
|
|
26
|
+
self,
|
|
27
|
+
code,
|
|
28
|
+
freq: str = "daily",
|
|
29
|
+
adjust: str = "none",
|
|
30
|
+
start_date: str = "",
|
|
31
|
+
end_date: str = "",
|
|
32
|
+
fields=None,
|
|
33
|
+
fill_data: bool = True,
|
|
34
|
+
) -> pd.DataFrame:
|
|
35
|
+
"""获取K线数据,返回标准 OHLCV 长表。
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
code: 证券代码或列表,如 ``"000001.SZ"`` 或 ``["000001.SZ", "600519.SH"]``。
|
|
39
|
+
freq: 频率:``1min/5min/15min/30min/60min/daily/weekly/monthly``。
|
|
40
|
+
adjust: 复权:``none`` 不复权 / ``qfq`` 前复权 / ``hfq`` 后复权。
|
|
41
|
+
start_date: 起始日期 ``YYYYMMDD``(分钟线可 ``YYYYMMDDHHMMSS``)。
|
|
42
|
+
end_date: 结束日期,留空取至今。
|
|
43
|
+
fields: 返回字段筛选(原字段名,如 ``["Open", "Close"]``)。
|
|
44
|
+
fill_data: 是否向后填充缺失数据。
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
DataFrame,列:``datetime, code, open, high, low, close, volume,
|
|
48
|
+
amount, forward_factor, open_interest``。
|
|
49
|
+
"""
|
|
50
|
+
stock_list = _as_list(code)
|
|
51
|
+
start_date = validate_date(start_date, "start_date")
|
|
52
|
+
end_date = validate_date(end_date, "end_date")
|
|
53
|
+
result = self._call(
|
|
54
|
+
"get_market_data",
|
|
55
|
+
stock_list=stock_list,
|
|
56
|
+
period=freq_to_period(freq),
|
|
57
|
+
count=-1,
|
|
58
|
+
dividend_type=adjust_to_dividend(adjust),
|
|
59
|
+
start_time=start_date or None,
|
|
60
|
+
end_time=end_date or None,
|
|
61
|
+
field_list=fields or None,
|
|
62
|
+
fill_data=fill_data,
|
|
63
|
+
)
|
|
64
|
+
return self._build_kline(result, stock_list)
|
|
65
|
+
|
|
66
|
+
def get_snapshot(self, code, fields=None, max_workers: int | None = 16) -> pd.DataFrame:
|
|
67
|
+
"""获取实时行情快照(返回扁平字段,索引 ``code``)。
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
code: 证券代码或列表。
|
|
71
|
+
fields: 返回字段筛选(原字段名)。
|
|
72
|
+
max_workers: 并发线程数。传入多个代码时默认并发(``16``),
|
|
73
|
+
单次约 3.5ms、全市场 5500+ 只约 5s。传 ``None``/``1`` 退回串行。
|
|
74
|
+
注意:通达信客户端服务端并行上限约 12~16,超过 20 会因排队变慢。
|
|
75
|
+
"""
|
|
76
|
+
codes = _as_list(code)
|
|
77
|
+
field_list = fields or None
|
|
78
|
+
|
|
79
|
+
def _fetch(c):
|
|
80
|
+
# 线程内只做 HTTP 拉取(纯 I/O、释放 GIL),字段重命名留到拉取后单遍处理,
|
|
81
|
+
# 避免 GIL 持有的 Python 计算在 16 线程下被串行化、吃掉并发红利
|
|
82
|
+
return self._call("get_market_snapshot", stock_code=c, field_list=field_list)
|
|
83
|
+
|
|
84
|
+
if not max_workers or max_workers <= 1 or len(codes) == 1:
|
|
85
|
+
results = [_fetch(c) for c in codes]
|
|
86
|
+
else:
|
|
87
|
+
# httpx.Client 线程安全,复用 self._client;ThreadPoolExecutor.map 保序
|
|
88
|
+
with ThreadPoolExecutor(max_workers=max_workers) as ex:
|
|
89
|
+
results = list(ex.map(_fetch, codes))
|
|
90
|
+
rows = [
|
|
91
|
+
self._dict_to_row(r, SNAPSHOT_FIELD_MAP, code=c)
|
|
92
|
+
for c, r in zip(codes, results)
|
|
93
|
+
]
|
|
94
|
+
return self._rows_to_df(rows, index_col="code")
|
|
95
|
+
|
|
96
|
+
def get_instrument_info(self, code, fields=None) -> pd.DataFrame:
|
|
97
|
+
"""证券基本信息(返回扁平字段,索引 ``code``)。"""
|
|
98
|
+
codes = _as_list(code)
|
|
99
|
+
rows = []
|
|
100
|
+
for c in codes:
|
|
101
|
+
result = self._call(
|
|
102
|
+
"get_stock_info", stock_code=c, field_list=fields or None
|
|
103
|
+
)
|
|
104
|
+
rows.append(self._dict_to_row(result, STOCK_INFO_FIELD_MAP, code=c))
|
|
105
|
+
return self._rows_to_df(rows, index_col="code")
|
|
106
|
+
|
|
107
|
+
def get_more_info(self, code, fields=None) -> pd.DataFrame:
|
|
108
|
+
"""证券更多信息(返回扁平字段,索引 ``code``)。
|
|
109
|
+
|
|
110
|
+
注意:HTTP 返回的数据在 ``result.Value`` 内。
|
|
111
|
+
"""
|
|
112
|
+
codes = _as_list(code)
|
|
113
|
+
rows = []
|
|
114
|
+
for c in codes:
|
|
115
|
+
result = self._call(
|
|
116
|
+
"get_more_info", stock_code=c, field_list=fields or None
|
|
117
|
+
)
|
|
118
|
+
data = result.get("Value", result)
|
|
119
|
+
rows.append(self._dict_to_row(data, STOCK_INFO_FIELD_MAP, code=c))
|
|
120
|
+
return self._rows_to_df(rows, index_col="code")
|
|
121
|
+
|
|
122
|
+
def get_dividend_factors(
|
|
123
|
+
self, code: str, start_date: str = "", end_date: str = ""
|
|
124
|
+
) -> pd.DataFrame:
|
|
125
|
+
"""获取分红送配(除权除息)数据。
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
DataFrame,列:``date, div_type, cash_dividend, rights_price,
|
|
129
|
+
share_bonus, rights_issue``。
|
|
130
|
+
"""
|
|
131
|
+
start_date = validate_date(start_date, "start_date")
|
|
132
|
+
end_date = validate_date(end_date, "end_date")
|
|
133
|
+
result = self._call(
|
|
134
|
+
"get_divid_factors",
|
|
135
|
+
stock_code=code,
|
|
136
|
+
start_time=start_date or None,
|
|
137
|
+
end_time=end_date or None,
|
|
138
|
+
)
|
|
139
|
+
return self._divid_to_df(result)
|
|
140
|
+
|
|
141
|
+
# ---------------- 内部工具 ----------------
|
|
142
|
+
def _divid_to_df(self, result: dict) -> pd.DataFrame:
|
|
143
|
+
"""解析 get_divid_factors 的 result。
|
|
144
|
+
|
|
145
|
+
结构:``{Date:[...], Type:[...], Value:[[bonus,allotprice,sharebonus,allotment],...]}``
|
|
146
|
+
"""
|
|
147
|
+
dates = result.get("Date", []) or []
|
|
148
|
+
types = result.get("Type", []) or []
|
|
149
|
+
values = result.get("Value", []) or []
|
|
150
|
+
rows = []
|
|
151
|
+
for i in range(len(dates)):
|
|
152
|
+
v = values[i] if i < len(values) else []
|
|
153
|
+
rows.append(
|
|
154
|
+
{
|
|
155
|
+
"date": dates[i],
|
|
156
|
+
"div_type": types[i] if i < len(types) else None,
|
|
157
|
+
"cash_dividend": v[0] if len(v) > 0 else None,
|
|
158
|
+
"rights_price": v[1] if len(v) > 1 else None,
|
|
159
|
+
"share_bonus": v[2] if len(v) > 2 else None,
|
|
160
|
+
"rights_issue": v[3] if len(v) > 3 else None,
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
if not rows:
|
|
164
|
+
return pd.DataFrame()
|
|
165
|
+
df = pd.DataFrame(rows)
|
|
166
|
+
for c in ("cash_dividend", "rights_price", "share_bonus", "rights_issue"):
|
|
167
|
+
df[c] = pd.to_numeric(df[c], errors="coerce")
|
|
168
|
+
return df
|
tdx_api/api/sector.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""板块 / 分类成分股接口。"""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
# get_stock_list 的分类代码(market) → 中文名称映射
|
|
8
|
+
STOCK_CATEGORY: dict[str, str] = {
|
|
9
|
+
"5": "所有A股",
|
|
10
|
+
"6": "上证指数成份股",
|
|
11
|
+
"7": "上证主板",
|
|
12
|
+
"8": "深证主板",
|
|
13
|
+
"9": "重点指数",
|
|
14
|
+
"10": "所有板块指数",
|
|
15
|
+
"11": "缺省行业板块",
|
|
16
|
+
"12": "概念板块",
|
|
17
|
+
"13": "风格板块",
|
|
18
|
+
"14": "地区板块",
|
|
19
|
+
"15": "行业+概念板块",
|
|
20
|
+
"16": "研究行业一级",
|
|
21
|
+
"17": "研究行业二级",
|
|
22
|
+
"18": "研究行业三级",
|
|
23
|
+
"21": "含H股",
|
|
24
|
+
"22": "含可转债",
|
|
25
|
+
"23": "沪深300",
|
|
26
|
+
"24": "中证500",
|
|
27
|
+
"25": "中证1000",
|
|
28
|
+
"26": "国证2000",
|
|
29
|
+
"27": "中证2000",
|
|
30
|
+
"28": "中证A500",
|
|
31
|
+
"30": "REITs",
|
|
32
|
+
"31": "ETF基金",
|
|
33
|
+
"32": "可转债",
|
|
34
|
+
"33": "LOF基金",
|
|
35
|
+
"34": "所有可交易基金",
|
|
36
|
+
"35": "所有沪深基金",
|
|
37
|
+
"36": "T+0基金",
|
|
38
|
+
"49": "金融类企业",
|
|
39
|
+
"50": "沪深A股",
|
|
40
|
+
"51": "创业板",
|
|
41
|
+
"52": "科创板",
|
|
42
|
+
"53": "北交所",
|
|
43
|
+
"101": "国内期货",
|
|
44
|
+
"102": "港股",
|
|
45
|
+
"103": "美股",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# market 不传时默认遍历的分类(股票 + 基金,不含期货/港股/美股/纯指数)
|
|
49
|
+
DEFAULT_STOCK_MARKETS = [
|
|
50
|
+
"5", "7", "8", "23", "24", "25", "31", "32", "50", "51", "52", "53",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SectorMixin:
|
|
55
|
+
"""板块与分类成分股接口。"""
|
|
56
|
+
|
|
57
|
+
def get_stock_list(self, market=None) -> pd.DataFrame:
|
|
58
|
+
"""获取股票/基金分类成分股。
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
market: 分类代码。不传(默认)则遍历所有主要分类合并返回;
|
|
62
|
+
传具体值(如 ``"5"``/``"23"``/``"32"``)只返回该分类。
|
|
63
|
+
完整取值见模块级 ``STOCK_CATEGORY``。
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
DataFrame,列:``code, market, category``(category 为中文分类名)。
|
|
67
|
+
``market`` 不传时,同一股票可能属于多个分类而出现多行。
|
|
68
|
+
"""
|
|
69
|
+
markets = DEFAULT_STOCK_MARKETS if market is None else [str(market)]
|
|
70
|
+
parts: list[pd.DataFrame] = []
|
|
71
|
+
for m in markets:
|
|
72
|
+
result = self._call("get_stock_list", market=m, list_type=0)
|
|
73
|
+
codes = result.get("Value", []) or []
|
|
74
|
+
if not codes:
|
|
75
|
+
continue
|
|
76
|
+
df = pd.DataFrame({"code": codes})
|
|
77
|
+
df["market"] = m
|
|
78
|
+
df["category"] = STOCK_CATEGORY.get(m, m)
|
|
79
|
+
parts.append(df)
|
|
80
|
+
if not parts:
|
|
81
|
+
return pd.DataFrame(columns=["code", "market", "category"])
|
|
82
|
+
return pd.concat(parts, ignore_index=True)
|
|
83
|
+
|
|
84
|
+
def get_sector_list(self, list_type: int = 0) -> pd.DataFrame:
|
|
85
|
+
"""获取 A 股全部板块。
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
list_type: ``0`` 仅代码(列 ``code``)/ ``1`` 含名称(列 ``code, name``)。
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
DataFrame。
|
|
92
|
+
"""
|
|
93
|
+
result = self._call("get_sector_list", list_type=list_type)
|
|
94
|
+
value = result.get("Value", []) or []
|
|
95
|
+
columns = ["code", "name"] if list_type else None
|
|
96
|
+
return self._list_to_df(value, columns=columns)
|
|
97
|
+
|
|
98
|
+
def get_user_sectors(self) -> pd.DataFrame:
|
|
99
|
+
"""获取用户自定义板块列表。
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
DataFrame,列:``code, name``(无自定义板块时为空)。
|
|
103
|
+
"""
|
|
104
|
+
result = self._call("get_user_sector")
|
|
105
|
+
value = result.get("Value")
|
|
106
|
+
if not value:
|
|
107
|
+
return pd.DataFrame(columns=["code", "name"])
|
|
108
|
+
return self._list_to_df(value, columns=["code", "name"])
|
|
109
|
+
|
|
110
|
+
def get_sector_stocks(
|
|
111
|
+
self, block_code: str, block_type: int = 0, list_type: int = 0
|
|
112
|
+
) -> pd.DataFrame:
|
|
113
|
+
"""获取板块成分股。
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
block_code: 板块代码(如 ``880081.SH``)或名称(如 ``钛金属``);
|
|
117
|
+
``block_type=1`` 时传自定义板块简称。
|
|
118
|
+
block_type: ``0`` 系统板块 / ``1`` 自定义板块简称。
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
单列 DataFrame,列名 ``code``。
|
|
122
|
+
"""
|
|
123
|
+
result = self._call(
|
|
124
|
+
"get_stock_list_in_sector",
|
|
125
|
+
block_code=block_code,
|
|
126
|
+
block_type=block_type,
|
|
127
|
+
list_type=list_type,
|
|
128
|
+
)
|
|
129
|
+
value = result.get("Value", []) or []
|
|
130
|
+
return self._list_to_df(value)
|
tdx_api/client.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""HTTP 客户端:直接调用通达信官方 HTTP 接口。
|
|
3
|
+
|
|
4
|
+
通达信量化客户端原生提供 HTTP 调用方式(无需 tqcenter / DLL / 自建网关)::
|
|
5
|
+
|
|
6
|
+
POST http://127.0.0.1:17709/
|
|
7
|
+
body : {"id": 1, "method": "<tqcenter 方法名>", "params": {...}}
|
|
8
|
+
响应 : {"id": 1, "result": {...}}
|
|
9
|
+
|
|
10
|
+
``method`` 为 tqcenter 中的函数名,``params`` 为底层参数名(多数与 tqcenter
|
|
11
|
+
参数名一致,少数不同,如财务类的 ``field_list`` 底层为 ``table_list``)。
|
|
12
|
+
|
|
13
|
+
本模块负责构造请求、解析响应、字段重命名、拼装 DataFrame。
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
import orjson
|
|
21
|
+
import pandas as pd
|
|
22
|
+
|
|
23
|
+
from .exceptions import TdxQuantConnectionError, TdxQuantError
|
|
24
|
+
|
|
25
|
+
# 通达信客户端官方 HTTP 端口
|
|
26
|
+
DEFAULT_BASE_URL = "http://127.0.0.1:17709"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _ClientBase:
|
|
30
|
+
"""HTTP 客户端基类,提供官方 HTTP 调用与 DataFrame 拼装工具。"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
35
|
+
timeout: float = 120.0,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.base_url = base_url.rstrip("/")
|
|
38
|
+
# trust_env=False:忽略系统/环境变量代理,直连同本机客户端
|
|
39
|
+
self._client = httpx.Client(
|
|
40
|
+
base_url=self.base_url, trust_env=False, timeout=timeout
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# ---------------- 生命周期 ----------------
|
|
44
|
+
def close(self) -> None:
|
|
45
|
+
self._client.close()
|
|
46
|
+
|
|
47
|
+
def __enter__(self) -> "_ClientBase":
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(self, *exc: Any) -> None:
|
|
51
|
+
self.close()
|
|
52
|
+
|
|
53
|
+
# ---------------- 核心调用 ----------------
|
|
54
|
+
def _call(self, method: str, **params: Any) -> dict:
|
|
55
|
+
"""POST 官方 HTTP 接口,返回 ``result`` 字典。
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
TdxQuantConnectionError: 网络/连接错误。
|
|
59
|
+
TdxQuantError: 通达信返回 ErrorId 非 0(含 .error_id / .error_msg)。
|
|
60
|
+
"""
|
|
61
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
62
|
+
try:
|
|
63
|
+
resp = self._client.post(
|
|
64
|
+
"/", json={"id": 1, "method": method, "params": params}
|
|
65
|
+
)
|
|
66
|
+
resp.raise_for_status()
|
|
67
|
+
except httpx.HTTPError as e:
|
|
68
|
+
raise TdxQuantConnectionError(
|
|
69
|
+
f"通达信 HTTP 连接失败({self.base_url}): {e}"
|
|
70
|
+
) from e
|
|
71
|
+
try:
|
|
72
|
+
# orjson 直接解析 resp.content(bytes),省一次 decode;响应小但调用频繁时累积收益明显
|
|
73
|
+
payload = orjson.loads(resp.content)
|
|
74
|
+
except ValueError as e:
|
|
75
|
+
raise TdxQuantError(f"返回非 JSON: {resp.text[:200]}") from e
|
|
76
|
+
result = payload.get("result", {})
|
|
77
|
+
if str(result.get("ErrorId", "0")) != "0":
|
|
78
|
+
raise TdxQuantError(
|
|
79
|
+
result.get("Error") or result.get("ErrorId") or "未知错误",
|
|
80
|
+
result.get("ErrorId"),
|
|
81
|
+
)
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
def health(self) -> dict:
|
|
85
|
+
"""健康检查:试调一个轻量接口验证连通性。"""
|
|
86
|
+
try:
|
|
87
|
+
self._call("get_trading_dates", market="SH", count=1)
|
|
88
|
+
return {"status": "ok", "base_url": self.base_url}
|
|
89
|
+
except Exception as e: # noqa: BLE001
|
|
90
|
+
return {"status": "error", "msg": str(e)}
|
|
91
|
+
|
|
92
|
+
# ---------------- DataFrame 拼装工具 ----------------
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _rename(mapping: dict[str, str], key: str) -> str:
|
|
95
|
+
"""按映射表重命名字段,未命中则转小写。"""
|
|
96
|
+
return mapping.get(key, key.lower())
|
|
97
|
+
|
|
98
|
+
def _dict_to_row(
|
|
99
|
+
self, raw: dict, mapping: dict[str, str], code: str | None = None
|
|
100
|
+
) -> dict:
|
|
101
|
+
"""把单只证券的 dict 重命名 key,返回一行数据。"""
|
|
102
|
+
row: dict[str, Any] = {}
|
|
103
|
+
if code is not None:
|
|
104
|
+
row["code"] = code
|
|
105
|
+
for k, v in raw.items():
|
|
106
|
+
if k in ("ErrorId", "Error", "run_id"):
|
|
107
|
+
continue
|
|
108
|
+
row[self._rename(mapping, k)] = v
|
|
109
|
+
return row
|
|
110
|
+
|
|
111
|
+
def _rows_to_df(
|
|
112
|
+
self, rows: list[dict], index_col: str | None = None
|
|
113
|
+
) -> pd.DataFrame:
|
|
114
|
+
if not rows:
|
|
115
|
+
return pd.DataFrame()
|
|
116
|
+
df = pd.DataFrame(rows)
|
|
117
|
+
for col in df.columns:
|
|
118
|
+
df[col] = self._maybe_numeric(df[col])
|
|
119
|
+
if index_col and index_col in df.columns:
|
|
120
|
+
df = df.set_index(index_col)
|
|
121
|
+
return df
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _maybe_numeric(series: pd.Series) -> pd.Series:
|
|
125
|
+
if series.map(lambda x: isinstance(x, (list, dict))).any():
|
|
126
|
+
return series
|
|
127
|
+
converted = pd.to_numeric(series, errors="coerce")
|
|
128
|
+
if converted.isna().all() and not series.isna().all():
|
|
129
|
+
return series
|
|
130
|
+
return converted
|
|
131
|
+
|
|
132
|
+
def _list_to_df(self, raw, columns=None) -> pd.DataFrame:
|
|
133
|
+
"""列表结果转 DataFrame(str→单列 / dict→自动成表 / list→多列)。"""
|
|
134
|
+
if not raw:
|
|
135
|
+
return pd.DataFrame()
|
|
136
|
+
first = raw[0]
|
|
137
|
+
if isinstance(first, str):
|
|
138
|
+
col = columns[0] if columns else "code"
|
|
139
|
+
return pd.DataFrame({col: raw})
|
|
140
|
+
if isinstance(first, dict):
|
|
141
|
+
return pd.DataFrame(raw)
|
|
142
|
+
df = pd.DataFrame(raw)
|
|
143
|
+
if columns and len(columns) >= df.shape[1]:
|
|
144
|
+
df = df.iloc[:, : len(columns)]
|
|
145
|
+
df.columns = columns
|
|
146
|
+
return df
|
|
147
|
+
|
|
148
|
+
def _build_kline(self, result: dict, stock_list) -> pd.DataFrame:
|
|
149
|
+
"""解析 get_market_data 的 result。
|
|
150
|
+
|
|
151
|
+
result.Value.{stock}.{field: [list]} → 标准 OHLCV 长表。
|
|
152
|
+
"""
|
|
153
|
+
from .schema import KLINE_FIELD_MAP
|
|
154
|
+
|
|
155
|
+
stock_list = list(stock_list)
|
|
156
|
+
value = result.get("Value", {})
|
|
157
|
+
parts: list[pd.DataFrame] = []
|
|
158
|
+
for stock in stock_list:
|
|
159
|
+
sv = value.get(stock)
|
|
160
|
+
if not sv:
|
|
161
|
+
continue
|
|
162
|
+
dates = sv.get("Date", [])
|
|
163
|
+
idx = pd.to_datetime(dates, errors="coerce") if dates else None
|
|
164
|
+
col_data: dict[str, Any] = {}
|
|
165
|
+
for field in (
|
|
166
|
+
"Open", "High", "Low", "Close",
|
|
167
|
+
"Volume", "Amount", "ForwardFactor", "VolInStock",
|
|
168
|
+
):
|
|
169
|
+
if field in sv:
|
|
170
|
+
col_data[field] = pd.to_numeric(
|
|
171
|
+
pd.Series(sv[field]), errors="coerce"
|
|
172
|
+
).values
|
|
173
|
+
if not col_data:
|
|
174
|
+
continue
|
|
175
|
+
sdf = pd.DataFrame(col_data, index=idx)
|
|
176
|
+
sdf.insert(0, "code", stock)
|
|
177
|
+
parts.append(sdf)
|
|
178
|
+
if not parts:
|
|
179
|
+
return pd.DataFrame()
|
|
180
|
+
out = pd.concat(parts)
|
|
181
|
+
out.index.name = "datetime"
|
|
182
|
+
out = out.reset_index()
|
|
183
|
+
out = out.rename(
|
|
184
|
+
columns=lambda c: KLINE_FIELD_MAP.get(c, c) if c != "datetime" else c
|
|
185
|
+
)
|
|
186
|
+
return out
|
tdx_api/enums.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""枚举与常量定义。
|
|
3
|
+
|
|
4
|
+
对外暴露的枚举使用金融行业常用写法(daily / 5min / qfq),
|
|
5
|
+
内部再转换为 tqcenter 要求的 period / dividend_type 字符串。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Freq(str, Enum):
|
|
14
|
+
"""K线频率(SDK 对外写法 → tqcenter period)。
|
|
15
|
+
|
|
16
|
+
使用 ``str`` 作为基类,因此可直接当字符串用:``Freq.DAILY == "daily"``。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
MIN1 = "1min" # 1分钟
|
|
20
|
+
MIN5 = "5min" # 5分钟
|
|
21
|
+
MIN15 = "15min" # 15分钟
|
|
22
|
+
MIN30 = "30min" # 30分钟
|
|
23
|
+
MIN60 = "60min" # 60分钟
|
|
24
|
+
DAILY = "daily" # 日线
|
|
25
|
+
WEEKLY = "weekly" # 周线
|
|
26
|
+
MONTHLY = "monthly" # 月线
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# SDK 频率 → tqcenter period 映射
|
|
30
|
+
FREQ_TO_PERIOD: dict[str, str] = {
|
|
31
|
+
"1min": "1m",
|
|
32
|
+
"5min": "5m",
|
|
33
|
+
"15min": "15m",
|
|
34
|
+
"30min": "30m",
|
|
35
|
+
"60min": "1h",
|
|
36
|
+
"daily": "1d",
|
|
37
|
+
"weekly": "1w",
|
|
38
|
+
"monthly": "1mon",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Adjust(str, Enum):
|
|
43
|
+
"""复权类型(SDK 对外写法 → tqcenter dividend_type)。"""
|
|
44
|
+
|
|
45
|
+
NONE = "none" # 不复权
|
|
46
|
+
QFQ = "qfq" # 前复权
|
|
47
|
+
HFQ = "hfq" # 后复权
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# SDK 复权 → tqcenter dividend_type 映射
|
|
51
|
+
ADJUST_TO_DIVIDEND: dict[str, str] = {
|
|
52
|
+
"none": "none",
|
|
53
|
+
"qfq": "front",
|
|
54
|
+
"hfq": "back",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Market(str, Enum):
|
|
59
|
+
"""市场后缀(证券代码 ``XXXXXX.XX`` 中的后缀部分)。"""
|
|
60
|
+
|
|
61
|
+
SH = "SH" # 上海
|
|
62
|
+
SZ = "SZ" # 深圳
|
|
63
|
+
BJ = "BJ" # 北交所
|
|
64
|
+
JJ = "JJ" # 基金
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def normalize_freq(freq: str) -> str:
|
|
68
|
+
"""把用户传入的频率统一成小写字符串。"""
|
|
69
|
+
return str(freq).lower()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def freq_to_period(freq: str) -> str:
|
|
73
|
+
"""SDK 频率 → tqcenter period,无效时抛 ValueError。"""
|
|
74
|
+
key = normalize_freq(freq)
|
|
75
|
+
if key not in FREQ_TO_PERIOD:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"不支持的频率: {freq!r},可选: {list(FREQ_TO_PERIOD.keys())}"
|
|
78
|
+
)
|
|
79
|
+
return FREQ_TO_PERIOD[key]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def normalize_adjust(adjust: str | None) -> str:
|
|
83
|
+
"""复权类型归一化,默认不复权。"""
|
|
84
|
+
if adjust is None:
|
|
85
|
+
return "none"
|
|
86
|
+
key = str(adjust).lower()
|
|
87
|
+
if key not in ADJUST_TO_DIVIDEND:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"不支持的复权类型: {adjust!r},可选: none / qfq / hfq"
|
|
90
|
+
)
|
|
91
|
+
return key
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def adjust_to_dividend(adjust: str | None) -> str:
|
|
95
|
+
"""SDK 复权 → tqcenter dividend_type。"""
|
|
96
|
+
return ADJUST_TO_DIVIDEND[normalize_adjust(adjust)]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# 严格日期格式:YYYYMMDD(日级)或 YYYYMMDDHHMMSS(分钟级)
|
|
100
|
+
_DATE_PATTERN = re.compile(r"^\d{8}$")
|
|
101
|
+
_DATETIME_PATTERN = re.compile(r"^\d{14}$")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def validate_date(value, name: str = "date") -> str:
|
|
105
|
+
"""严格校验日期格式为 ``YYYYMMDD``(或分钟线的 ``YYYYMMDDHHMMSS``)。
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
value: 日期值。``None`` / 空串视为"不限制",原样返回。
|
|
109
|
+
name: 参数名,用于报错信息。
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
校验通过的字符串。
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ValueError: 格式不符合 ``YYYYMMDD``。
|
|
116
|
+
"""
|
|
117
|
+
if value is None or value == "":
|
|
118
|
+
return value if value is not None else ""
|
|
119
|
+
s = str(value)
|
|
120
|
+
if _DATE_PATTERN.match(s) or _DATETIME_PATTERN.match(s):
|
|
121
|
+
return s
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"{name} 格式错误: {value!r},应为 YYYYMMDD(如 20260613)"
|
|
124
|
+
)
|
tdx_api/exceptions.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""SDK 异常定义。"""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TdxQuantError(Exception):
|
|
7
|
+
"""通达信量化 SDK 通用错误(业务错误,error_id 非 0)。"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, error_id: str | None = None) -> None:
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.error_id = error_id
|
|
12
|
+
self.error_msg = message
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TdxQuantConnectionError(TdxQuantError):
|
|
16
|
+
"""网关连接错误(网络不可达、超时等)。"""
|
tdx_api/schema.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""字段血缘映射表(字段重命名的唯一真实来源)。
|
|
3
|
+
|
|
4
|
+
本文件记录 tqcenter 原始字段名 → SDK 规范字段名(snake_case 金融行业惯例)的映射。
|
|
5
|
+
``docs/field_lineage.md`` 据此生成,保证代码与文档一致。
|
|
6
|
+
|
|
7
|
+
设计原则:
|
|
8
|
+
- 价格类:open / high / low / close / last / last_close / average_price
|
|
9
|
+
- 量额类:volume(手) / amount(元或万元) / turnover
|
|
10
|
+
- 五档买卖:bid_prices / bid_volumes / ask_prices / ask_volumes
|
|
11
|
+
- 基金:nav(净值)
|
|
12
|
+
- 涨跌:pct_chg(涨跌幅) / change_speed(涨速)
|
|
13
|
+
- 期货:open_interest(持仓量)
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
# ----------------------------------------------------------------------
|
|
18
|
+
# K线字段:tqcenter get_market_data 返回 {field: DataFrame(index=stock, col=time)}
|
|
19
|
+
# SDK 转换为长表,每行一个时间点。
|
|
20
|
+
# ----------------------------------------------------------------------
|
|
21
|
+
KLINE_FIELD_MAP: dict[str, str] = {
|
|
22
|
+
"Date": "date",
|
|
23
|
+
"Time": "time",
|
|
24
|
+
"Open": "open",
|
|
25
|
+
"High": "high",
|
|
26
|
+
"Low": "low",
|
|
27
|
+
"Close": "close",
|
|
28
|
+
"Volume": "volume",
|
|
29
|
+
"Amount": "amount",
|
|
30
|
+
"ForwardFactor": "forward_factor", # 前复权因子(仅不复权时有效)
|
|
31
|
+
"VolInStock": "open_interest", # 持仓量(期货)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# ----------------------------------------------------------------------
|
|
35
|
+
# 行情快照字段:tqcenter get_market_snapshot 返回 dict
|
|
36
|
+
# ----------------------------------------------------------------------
|
|
37
|
+
SNAPSHOT_FIELD_MAP: dict[str, str] = {
|
|
38
|
+
"ItemNum": "tick_count", # 快照笔数
|
|
39
|
+
"LastClose": "last_close", # 昨收价
|
|
40
|
+
"Open": "open", # 开盘价
|
|
41
|
+
"Max": "high", # 最高价
|
|
42
|
+
"Min": "low", # 最低价
|
|
43
|
+
"Now": "last", # 最新价(现价)
|
|
44
|
+
"Volume": "volume", # 总成交量(手)
|
|
45
|
+
"NowVol": "last_volume", # 现手
|
|
46
|
+
"Amount": "amount", # 总成交额
|
|
47
|
+
"Inside": "inside_volume", # 内盘
|
|
48
|
+
"Outside": "outside_volume", # 外盘
|
|
49
|
+
"TickDiff": "tick_change", # 笔涨跌
|
|
50
|
+
"InOutFlag": "in_out_flag", # 内外盘标志 0买1卖2未知
|
|
51
|
+
"Jjjz": "nav", # 基金净值
|
|
52
|
+
"Buyp": "bid_prices", # 五档买价(list)
|
|
53
|
+
"Buyv": "bid_volumes", # 五档买量(list)
|
|
54
|
+
"Sellp": "ask_prices", # 五档卖价(list)
|
|
55
|
+
"Sellv": "ask_volumes", # 五档卖量(list)
|
|
56
|
+
"UpHome": "up_count", # 上涨家数(指数)
|
|
57
|
+
"DownHome": "down_count", # 下跌家数(指数)
|
|
58
|
+
"Before5MinNow": "price_5min_ago", # 5分钟前价格
|
|
59
|
+
"Average": "average_price", # 均价
|
|
60
|
+
"XsFlag": "decimal_places", # 小数位数
|
|
61
|
+
"Zangsu": "change_speed", # 涨速
|
|
62
|
+
"ZAFPre3": "pct_chg_3d", # 3日涨幅(%)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# ----------------------------------------------------------------------
|
|
66
|
+
# 证券基本信息字段:tqcenter get_stock_info 返回 dict
|
|
67
|
+
# 字段以实际返回为准,此处覆盖文档已知项;未知原字段保留原名转小写。
|
|
68
|
+
# ----------------------------------------------------------------------
|
|
69
|
+
STOCK_INFO_FIELD_MAP: dict[str, str] = {
|
|
70
|
+
"Code": "code",
|
|
71
|
+
"Name": "name",
|
|
72
|
+
"J_zgb": "total_share_capital", # 总股本
|
|
73
|
+
"ActiveCapital": "float_capital", # 流通股本
|
|
74
|
+
"J_zltb": "float_ratio", # 流通占比
|
|
75
|
+
"Industry": "industry", # 行业
|
|
76
|
+
"J_hy": "industry_code",
|
|
77
|
+
"J_zh": "concept_code",
|
|
78
|
+
"J_country": "region",
|
|
79
|
+
"J_main": "main_business",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# ----------------------------------------------------------------------
|
|
83
|
+
# 分红送配字段:tqcenter get_divid_factors 返回 DataFrame
|
|
84
|
+
# ----------------------------------------------------------------------
|
|
85
|
+
DIVIDEND_FIELD_MAP: dict[str, str] = {
|
|
86
|
+
# 实际字段(index 为除权除息日期)
|
|
87
|
+
"Type": "div_type", # 分红类型
|
|
88
|
+
"Bonus": "cash_dividend", # 每股派息(红利)
|
|
89
|
+
"AllotPrice": "rights_price", # 配股价
|
|
90
|
+
"ShareBonus": "share_bonus", # 每股送转股
|
|
91
|
+
"Allotment": "rights_issue", # 每股配股
|
|
92
|
+
"ForwardFactor": "forward_factor",
|
|
93
|
+
"BackwardFactor": "backward_factor",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# 可转债字段:tqcenter get_kzz_info 返回 dict
|
|
97
|
+
CB_FIELD_MAP: dict[str, str] = {
|
|
98
|
+
"SetCode": "market", # 市场 0深1沪
|
|
99
|
+
"KZZCode": "bond_code", # 可转债代码
|
|
100
|
+
"HSCode": "underlying_code", # 正股代码
|
|
101
|
+
"ZGPrice": "conversion_price", # 转股价
|
|
102
|
+
"CurRate": "coupon_rate", # 票面利率
|
|
103
|
+
"RestScope": "remaining_scale", # 剩余规模
|
|
104
|
+
"PutBack": "putback_flag", # 回售标志
|
|
105
|
+
"ForceRedeem": "force_redeem_flag", # 强制赎回标志
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# ETF 字段:tqcenter get_trackzs_etf_info 返回 list[dict]
|
|
109
|
+
ETF_FIELD_MAP: dict[str, str] = {
|
|
110
|
+
"Code": "code",
|
|
111
|
+
"Name": "name",
|
|
112
|
+
"NowPrice": "last_price", # 现价
|
|
113
|
+
"PreClose": "pre_close", # 昨收
|
|
114
|
+
"IOPV": "iopv", # 参考净值
|
|
115
|
+
"Zgb": "total_share", # 总份额/规模
|
|
116
|
+
"Sz": "scale", # 规模
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# 新股/新债申购字段:tqcenter get_ipo_info 返回 list[dict]
|
|
120
|
+
IPO_FIELD_MAP: dict[str, str] = {
|
|
121
|
+
"SetCode": "market", # 市场 0深1沪
|
|
122
|
+
"Code": "code",
|
|
123
|
+
"Name": "name",
|
|
124
|
+
"SGDate": "subscribe_date", # 申购日期
|
|
125
|
+
"SGPrice": "subscribe_price", # 申购价
|
|
126
|
+
"SGCode": "subscribe_code", # 申购代码
|
|
127
|
+
"MaxSG": "max_subscribe", # 最大申购额
|
|
128
|
+
"PE_Issue": "issue_pe", # 发行市盈率
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# 板块字段:tqcenter get_sector_list 返回 list
|
|
132
|
+
# 元素为 [code, name] 形式,SDK 转为 DataFrame(code, name)
|
|
133
|
+
SECTOR_FIELD_MAP: dict[str, str] = {
|
|
134
|
+
"code": "code",
|
|
135
|
+
"name": "name",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def rename_keys(data: dict, mapping: dict[str, str]) -> dict:
|
|
140
|
+
"""按映射表重命名字典的 key;未命中的 key 原样保留。
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
data: 原始字典。
|
|
144
|
+
mapping: {原字段: 新字段} 映射。
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
重命名后的新字典。
|
|
148
|
+
"""
|
|
149
|
+
return {mapping.get(k, k): v for k, v in data.items()}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def rename_dataframe_columns(
|
|
153
|
+
df, mapping: dict[str, str], keep_unknown: bool = True
|
|
154
|
+
):
|
|
155
|
+
"""重命名 DataFrame 的列名。
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
df: pandas.DataFrame。
|
|
159
|
+
mapping: {原列名: 新列名}。
|
|
160
|
+
keep_unknown: True 时未命中的列保留原名;False 时丢弃。
|
|
161
|
+
"""
|
|
162
|
+
import pandas as pd
|
|
163
|
+
|
|
164
|
+
if df is None or df.empty:
|
|
165
|
+
return df
|
|
166
|
+
if not keep_unknown:
|
|
167
|
+
df = df[[c for c in df.columns if c in mapping]]
|
|
168
|
+
return df.rename(columns={k: v for k, v in mapping.items() if k in df.columns})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tdxquant
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 通达信量化(TdxQuant) HTTP SDK —— 统一返回 DataFrame,字段金融规范化(snake_case)
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: httpx>=0.27
|
|
7
|
+
Requires-Dist: orjson>=3.11.9
|
|
8
|
+
Requires-Dist: pandas>=2.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# tdx-api
|
|
12
|
+
|
|
13
|
+
通达信量化(TdxQuant)**HTTP SDK** —— 直接调用通达信客户端官方 HTTP 接口(`POST http://127.0.0.1:17709/`),统一返回 `pandas.DataFrame`,字段重命名为金融行业规范的 **snake_case**。
|
|
14
|
+
|
|
15
|
+
## 工作方式
|
|
16
|
+
|
|
17
|
+
通达信量化客户端原生提供 HTTP 调用方式(无需 tqcenter / DLL / 自建网关):
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
你的代码(SDK) ──HTTP/JSON──► 通达信客户端(127.0.0.1:17709)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
1. 启动支持 TQ 的通达信客户端(它会监听 `127.0.0.1:17709`)
|
|
24
|
+
2. SDK 直接 POST 调用,字段重命名 + DataFrame 封装在 SDK 内完成
|
|
25
|
+
|
|
26
|
+
> 请求格式:`{"id":1, "method":"<tqcenter方法名>", "params":{...}}`,`method` 为 tqcenter 函数名,`params` 为底层参数名。
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv sync # 本地开发
|
|
32
|
+
# 或 pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> 需要 Python ≥ 3.13、通达信金融终端(量化模拟版 / 专业研究版等支持 TQ 策略的版本)。
|
|
36
|
+
|
|
37
|
+
## 快速开始
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from tdx_api import TdxAPI
|
|
41
|
+
|
|
42
|
+
api = TdxAPI() # 默认 http://127.0.0.1:17709,需通达信客户端在线
|
|
43
|
+
|
|
44
|
+
# K线(OHLCV 长表)
|
|
45
|
+
df = api.get_kline("000001.SZ", freq="daily", adjust="qfq", start_date="20260101")
|
|
46
|
+
# 实时快照
|
|
47
|
+
snap = api.get_snapshot("000001.SZ")
|
|
48
|
+
# 股票 / 板块 / ETF
|
|
49
|
+
api.get_stock_list() # 所有分类合并(带 market/category 列)
|
|
50
|
+
api.get_sector_list()
|
|
51
|
+
api.get_track_etf("000300.SH")
|
|
52
|
+
# 财务
|
|
53
|
+
api.get_financial("600519.SH", fields=["Fn193","Fn194"], start_date="20240101")
|
|
54
|
+
# 交易日历
|
|
55
|
+
api.get_trading_calendar("20260101", "20260613")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
从别的机器调用:`TdxAPI(base_url="http://通达信机器IP:17709")`。
|
|
59
|
+
|
|
60
|
+
## 接口总览
|
|
61
|
+
|
|
62
|
+
| 类别 | 方法 | 说明 |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| 行情 | `get_kline` | K线(OHLCV 长表) |
|
|
65
|
+
| | `get_snapshot` | 实时快照(五档) |
|
|
66
|
+
| | `get_instrument_info` / `get_more_info` | 基本信息 |
|
|
67
|
+
| | `get_dividend_factors` | 分红送配 |
|
|
68
|
+
| 财务 | `get_financial` / `get_financial_by_date` | 专业财务(FNxxx) |
|
|
69
|
+
| | `get_stock_trade_data(_by_date)` | 股票交易(GPx) |
|
|
70
|
+
| | `get_sector_trade_data(_by_date)` | 板块交易(BKx) |
|
|
71
|
+
| | `get_market_trade_data(_by_date)` | 市场交易(SCx) |
|
|
72
|
+
| | `get_stock_single_data` | 单点数据(GOx) |
|
|
73
|
+
| | `get_share_capital(_by_date)` | 股本数据 |
|
|
74
|
+
| 板块 | `get_stock_list` | 分类成分股(market 不传返回全部,带 market/category 列) |
|
|
75
|
+
| | `get_sector_list` / `get_user_sectors` | 板块列表 |
|
|
76
|
+
| | `get_sector_stocks` | 板块成分股 |
|
|
77
|
+
| 证券 | `get_convertible_bond` | 可转债 |
|
|
78
|
+
| | `get_ipo_info` | 新股/新债申购 |
|
|
79
|
+
| | `get_track_etf` | 跟踪指数的 ETF |
|
|
80
|
+
| 日历 | `get_trading_dates` / `get_trading_calendar` | 交易日 |
|
|
81
|
+
|
|
82
|
+
> 交易类(`order_stock`)按需求不做。完整参数与字段说明见 [`docs/api_reference.md`](docs/api_reference.md)。
|
|
83
|
+
|
|
84
|
+
## 字段命名规范
|
|
85
|
+
|
|
86
|
+
- 全部 **snake_case 小写**,采用金融行业惯例(OHLCV / bid-ask / nav / pct_chg)。
|
|
87
|
+
- 原字段 → 新字段完整血缘见 [`docs/field_lineage.md`](docs/field_lineage.md)。
|
|
88
|
+
- 通达信专业编码字段(`FNxxx`/`GPx` 等)保留转小写,需对照通达信字段手册。
|
|
89
|
+
|
|
90
|
+
代码格式:`000001.SZ` / `600519.SH`(6 位 + 市场后缀);频率 `daily/5min/...`;
|
|
91
|
+
复权 `none/qfq/hfq`;日期严格 `YYYYMMDD`(区间用 `start_date`/`end_date`,按日期点用 `date`)。
|
|
92
|
+
|
|
93
|
+
## 测试
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
uv run pytest # 单元测试(字段血缘契约 + 拼装逻辑)
|
|
97
|
+
uv run pytest --runintegration # 集成测试(需通达信客户端在线)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 项目结构
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
tdx_api/
|
|
104
|
+
├── client.py # HTTP 客户端(POST 17709)+ DataFrame 拼装
|
|
105
|
+
├── schema.py # 字段血缘映射表(唯一真实来源)
|
|
106
|
+
├── enums.py # Freq / Adjust 枚举 + validate_date 日期校验
|
|
107
|
+
├── exceptions.py # 异常
|
|
108
|
+
└── api/ # 各业务接口(mixin)
|
|
109
|
+
├── market.py financial.py sector.py instrument.py calendar.py
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 常见问题
|
|
113
|
+
|
|
114
|
+
- **`server return none` / 连接失败**:确认通达信客户端已启动并登录行情;SDK 默认连 `127.0.0.1:17709`。
|
|
115
|
+
- **`502 Bad Gateway`**:本机代理(Clash/V2Ray)劫持 localhost 请求。SDK 已默认 `trust_env=False` 直连,若仍异常请关闭代理。
|
|
116
|
+
- **财务/专业数据为空**:需先在通达信客户端下载对应盘后/专业财务数据。
|
|
117
|
+
- **`get_financial` 报 end_time 缺失**:底层要求结束日期,`end_date` 留空时 SDK 自动取当天。
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
tdx_api/__init__.py,sha256=unm9Wof0jH3PVi44LnHeIYMDoSNAIhPP_L-J5ekLhVY,1653
|
|
2
|
+
tdx_api/client.py,sha256=snxqm-4SL6gRNrCmSHHn4CjprHCct2G9Nlk14LkhERE,6769
|
|
3
|
+
tdx_api/enums.py,sha256=yq3eZzyeGNDlXlOr6uns9bIQEUgl3FejyvKEHyhq2UU,3347
|
|
4
|
+
tdx_api/exceptions.py,sha256=uf4lJd_obfKMyWnKa2Fjp_AC8vaLzaTOqi39JLgbYvc,489
|
|
5
|
+
tdx_api/schema.py,sha256=4Ge1mDI9LpR7B_zrBETNPg9yY8-1i3AbebgtgCcusW4,6696
|
|
6
|
+
tdx_api/api/__init__.py,sha256=60yb9xd8Y3jFLTPCiAfH_uy3d5BUcE-dwe2GGvpaUyk,456
|
|
7
|
+
tdx_api/api/calendar.py,sha256=WPgM0-TtHB2CO0fyfUvJds9vNejmjPbVlMMk1g5p5nE,1880
|
|
8
|
+
tdx_api/api/financial.py,sha256=9Bgwl4Pi8eyS1r6KD9qMvBmRKyefv33kC0exySawQ3I,8280
|
|
9
|
+
tdx_api/api/instrument.py,sha256=GwOjeWnBOzRafeDeA4lez3Hg8uAPDVS518pZzI5jwiw,2227
|
|
10
|
+
tdx_api/api/market.py,sha256=foXfF_w2H_FoWUito7H7nofKGn7IEr99_CBwrd7rSDo,6709
|
|
11
|
+
tdx_api/api/sector.py,sha256=ggXM1zf_iGfbW2Tv1HaYw7E7rmonGuGJySidn7m9X8k,4340
|
|
12
|
+
tdxquant-0.1.0.dist-info/METADATA,sha256=Q-3NzioXkXAMJAmnBrZiG6CO_ParxSBkSLAlAb0yQ3k,4828
|
|
13
|
+
tdxquant-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
14
|
+
tdxquant-0.1.0.dist-info/RECORD,,
|