Skip to content

Commit 9438946

Browse files
shinny-hongyanshinny-chenli
authored andcommitted
Update Version 3.9.0
1 parent 6a54560 commit 9438946

17 files changed

+428
-23
lines changed

PKG-INFO

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Metadata-Version: 2.1
22
Name: tqsdk
3-
Version: 3.8.9
3+
Version: 3.9.0
44
Summary: TianQin SDK
55
Home-page: https://www.shinnytech.com/tqsdk
66
Author: TianQin

doc/conf.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,17 @@
4040

4141
# General information about the project.
4242
project = u'TianQin Python SDK'
43-
copyright = u'2018-2025, TianQin'
43+
copyright = u'2018-2026, TianQin'
4444
author = u'TianQin'
4545

4646
# The version info for the project you're documenting, acts as replacement for
4747
# |version| and |release|, also used in various other places throughout the
4848
# built documents.
4949
#
5050
# The short X.Y version.
51-
version = u'3.8.9'
51+
version = u'3.9.0'
5252
# The full version, including alpha/beta/rc tags.
53-
release = u'3.8.9'
53+
release = u'3.9.0'
5454

5555
# The language for content autogenerated by Sphinx. Refer to documentation
5656
# for a list of supported languages.

doc/version.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
版本变更
44
=============================
5+
3.9.0 (2026/01/16)
6+
7+
* 新增: :py:meth:`~tqsdk.TqApi.query_edb_data` 接口,获取非量价数据
8+
9+
510
3.8.9 (2025/12/23)
611

712
* 修复: 回测/下载数据在行情服务运维阶段发生断线重连,重连成功后没有发送已订阅请求,导致回测/下载数据无法继续推进的问题

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
setuptools.setup(
1010
name='tqsdk',
11-
version="3.8.9",
11+
version="3.9.0",
1212
description='TianQin SDK',
1313
author='TianQin',
1414
author_email='tianqincn@gmail.com',

tqsdk/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '3.8.9'
1+
__version__ = '3.9.0'

tqsdk/api.py

Lines changed: 163 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
from tqsdk.objs import Quote, TradingStatus, Kline, Tick, Account, Position, Order, Trade, RiskManagementRule, RiskManagementData
6868
from tqsdk.objs import SecurityAccount, SecurityOrder, SecurityTrade, SecurityPosition
6969
from tqsdk.objs_not_entity import QuoteList, TqDataFrame, TqSymbolDataFrame, SymbolList, SymbolLevelList, \
70-
TqSymbolRankingDataFrame, TqOptionGreeksDataFrame, TqMdSettlementDataFrame
70+
TqSymbolRankingDataFrame, TqOptionGreeksDataFrame, TqMdSettlementDataFrame, TqEdbIndexDataFrame
7171
from tqsdk.risk_manager import TqRiskManager
7272
from tqsdk.risk_rule import TqRiskRule
7373
from tqsdk.ins_schema import ins_schema, basic, derivative, future, option
@@ -251,6 +251,7 @@ def __init__(self, account: Optional[Union[TqMultiAccount, UnionTradeable]] = No
251251
"quotes": set(),
252252
"klines": {},
253253
"ticks": {},
254+
"edb": {}, # 记录已发出的 EDB 请求(key: 参数元组, value: DataFrame 对象),用于请求去重/复用
254255
} # 记录已发出的请求
255256
self._serials = {} # 记录所有数据序列
256257
# 记录所有(若有多个serial 则仅data_length不同, right_id相同)合约、周期相同的多合约K线中最大的更新数据范围
@@ -2375,6 +2376,167 @@ def query_symbol_settlement(self, symbol: Union[str, List[str]], days: int = 1,
23752376
raise TqTimeoutError("获取每日结算价信息超时,请检查客户端及网络是否正常")
23762377
return df
23772378

2379+
def query_edb_data(self, ids: List[int], n: int = 1, align: Optional[str] = None, fill: Optional[str] = None) -> TqEdbIndexDataFrame:
2380+
"""
2381+
查询非量价指标数据
2382+
2383+
.. warning::
2384+
这是一个实验性接口(**unstable**):EDB 后端的 id/表结构/数据口径可能变更,
2385+
本接口的参数与返回结构未来可能调整,届时可能不兼容旧代码。
2386+
2387+
EDB 指标数据说明:
2388+
- EDB 指标数据网站入口:`https://edb.shinnytech.com`
2389+
- 指标 id 列表、指标含义、以及数据可视化展示,可在上述网站中在线查询与查看
2390+
2391+
Args:
2392+
ids (list[int]): [必填] 指标 id 列表,长度 1~100(会去重并保持请求顺序)
2393+
n (int): [可选] 时间窗口长度(单位:自然日,闭区间),默认 1。
2394+
- end 使用当前 api 时间(回测模式下为回测推进的当前时间对应日期)
2395+
- start = end - (n - 1) 天
2396+
align (str/None): [可选] 对齐方式:
2397+
- None [默认]:仅返回实际有值的日期(稀疏),不补齐缺失日期
2398+
- "day":在 [start, end] 内按自然日补齐日期
2399+
fill (str): [可选] 填充方式(仅在 align="day" 时生效):
2400+
- None [默认]:不填充,缺失为 NaN
2401+
- "ffill":用前一个已知值向后填充
2402+
- "bfill":用后一个已知值向前填充
2403+
2404+
Returns:
2405+
pandas.DataFrame: 本函数返回 pandas.DataFrame 实例。返回值不会再更新。包含以下结构说明:
2406+
2407+
* index: 日期字符串,格式为 YYYY-MM-DD
2408+
* align=None 时:仅包含实际有值的日期(稀疏),不补齐缺失日期
2409+
* align="day" 时:包含 [start, end] 内的所有自然日(补齐缺失日期)
2410+
* columns: 指标 id(会对 ids 去重并保持请求顺序;最终以服务端返回为准)
2411+
* values: 指标取值
2412+
* 缺失值为 NaN
2413+
* align="day" 且 fill="ffill"/"bfill" 时,会对缺失值进行前向/后向填充
2414+
2415+
Example (实盘)::
2416+
2417+
from tqsdk import TqApi, TqAuth
2418+
2419+
api = TqApi(auth=TqAuth("快期账号", "快期密码"))
2420+
try:
2421+
# Case 1: 最小调用(只传 ids,其余用默认)
2422+
# - n 默认为 1:窗口为 [end, end]
2423+
# - align=None:稀疏返回,不补齐日期
2424+
df1 = api.query_edb_data(ids=[472])
2425+
2426+
# Case 2: ids 去重并保持顺序(重复的 472 会被去掉)
2427+
df2 = api.query_edb_data(ids=[472, 497, 472])
2428+
2429+
# Case 3: 指定 n(自然日,闭区间)
2430+
# - end 为 api 当前日期(回测模式下为回测推进到的当前日期)
2431+
# - start = end - (n-1)
2432+
df3 = api.query_edb_data(ids=[472, 497], n=10)
2433+
2434+
# Case 4: align=None(稀疏,不补齐),fill 会被忽略
2435+
df4 = api.query_edb_data(ids=[472, 497, 10350], n=10, align=None, fill=None)
2436+
2437+
# Case 5: align="day"(自然日补齐),不填充缺失(NaN)
2438+
df5 = api.query_edb_data(ids=[472, 497, 10350], n=10, align="day", fill=None)
2439+
2440+
# Case 6: align="day" + fill="ffill"(前值填充)
2441+
df6 = api.query_edb_data(ids=[472, 497, 10350], n=10, align="day", fill="ffill")
2442+
2443+
# Case 7: align="day" + fill="bfill"(后值填充)
2444+
df7 = api.query_edb_data(ids=[472, 497, 10350], n=10, align="day", fill="bfill")
2445+
2446+
print(df1.to_string())
2447+
print(df2.to_string())
2448+
print(df3.to_string())
2449+
print(df4.to_string())
2450+
print(df5.to_string())
2451+
print(df6.to_string())
2452+
print(df7.to_string())
2453+
finally:
2454+
api.close()
2455+
2456+
Example (回测)::
2457+
2458+
from datetime import datetime
2459+
from tqsdk import TqApi, TqAuth, TqBacktest
2460+
2461+
api = TqApi(
2462+
auth=TqAuth("快期账号", "快期密码"),
2463+
backtest=TqBacktest(start_dt=datetime(2025, 10, 1), end_dt=datetime(2026, 1, 12)),
2464+
)
2465+
klines = api.get_kline_serial(symbol="SHFE.ni2512", duration_seconds=86400, data_length=10)
2466+
try:
2467+
# 初次查询:以当前回测推进到的日期作为 end
2468+
edb = api.query_edb_data(ids=[472, 497, 10350], n=10, align="day", fill="ffill")
2469+
print(edb.to_string())
2470+
# 之后随着回测时间推进,end 会变化
2471+
while True:
2472+
api.wait_update()
2473+
if api.is_changing(klines.iloc[-1], "datetime"):
2474+
edb = api.query_edb_data(ids=[472, 497, 10350], n=10, align="day", fill="ffill")
2475+
print(edb.to_string())
2476+
finally:
2477+
api.close()
2478+
"""
2479+
if not isinstance(ids, list) or len(ids) == 0:
2480+
raise Exception("ids 不能为空。")
2481+
if not isinstance(n, int) or n < 1:
2482+
raise Exception(f"n 参数 {n} 错误,应为 >= 1 的整数。")
2483+
if align not in (None, "day"):
2484+
raise Exception(f"align 参数 {align} 错误,仅支持 None 或 'day'")
2485+
if fill not in (None, "ffill", "bfill"):
2486+
raise Exception(f"fill 参数 {fill} 错误,仅支持 None/'ffill'/'bfill'")
2487+
2488+
# 以 api 当前时间计算窗口(回测模式下会随回测推进)
2489+
now_dt = self._get_current_datetime()
2490+
# backtest 初始化早期 current_dt 可能为 0,兜底到 backtest start_dt
2491+
if isinstance(self._backtest, TqBacktest) and getattr(now_dt, "year", 1970) <= 1970:
2492+
now_dt = _timestamp_nano_to_datetime(self._backtest._start_dt)
2493+
end_date = now_dt.date()
2494+
start_date = end_date - timedelta(days=n - 1)
2495+
start_s = start_date.strftime("%Y-%m-%d")
2496+
end_s = end_date.strftime("%Y-%m-%d")
2497+
2498+
# ids 归一化:去重保持顺序
2499+
seen = set()
2500+
norm_ids = []
2501+
for x in ids:
2502+
if not isinstance(x, int):
2503+
raise Exception(f"ids 中包含非法 id: {x} (仅支持 int)")
2504+
v = x
2505+
if v not in seen:
2506+
seen.add(v)
2507+
norm_ids.append(v)
2508+
if len(norm_ids) > 100:
2509+
raise Exception("ids 数量超过限制(<=100)")
2510+
2511+
edb_cache = self._requests["edb"]
2512+
2513+
# 统一策略(实盘/回测):
2514+
# - api._requests 只缓存“原始数据”(HTTP 拉取得到的稀疏日期数据),缓存 key 不包含 align/fill,避免重复下载
2515+
# - 返回给用户时再按窗口切片,并做 align/fill(回测下先切片再 fill,避免引入未来数据)
2516+
if isinstance(self._backtest, TqBacktest):
2517+
bt_start_d = _timestamp_nano_to_datetime(self._backtest._start_dt).date()
2518+
bt_end_d = _timestamp_nano_to_datetime(self._backtest._end_dt).date()
2519+
raw_start_s = (bt_start_d - timedelta(days=n - 1)).strftime("%Y-%m-%d")
2520+
raw_end_s = bt_end_d.strftime("%Y-%m-%d")
2521+
else:
2522+
raw_start_s = start_s
2523+
raw_end_s = end_s
2524+
2525+
raw_request = (tuple(norm_ids), raw_start_s, raw_end_s)
2526+
raw_df = edb_cache.get(raw_request, None)
2527+
if raw_df is None:
2528+
raw_df = TqEdbIndexDataFrame(self, ids=norm_ids, start=raw_start_s, end=raw_end_s,
2529+
align=None, fill=None)
2530+
edb_cache[raw_request] = raw_df
2531+
2532+
# 每次调用返回窗口 df(不缓存窗口对象:同一个 raw 可派生出多种 align/fill)
2533+
df = TqEdbIndexDataFrame(self, ids=norm_ids, start=start_s, end=end_s, align=align, fill=fill, raw_df=raw_df)
2534+
deadline = time.time() + 30
2535+
while not self._loop.is_running() and not df.__dict__["_task"].done():
2536+
if not self.wait_update(deadline=deadline, _task=df.__dict__["_task"]):
2537+
raise TqTimeoutError("获取 EDB 指标取值超时,请检查客户端及网络是否正常")
2538+
return df
2539+
23782540
def query_symbol_ranking(self, symbol: str, ranking_type: str, days: int = 1, start_dt: date = None, broker: str = None)\
23792541
-> TqSymbolRankingDataFrame:
23802542
"""

tqsdk/auth.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ def _request_token(self, payload):
118118
data = {"client_id": "shinny_tq", "client_secret": "be30b9f4-6862-488a-99ad-21bde0400081"}
119119
data.update(payload)
120120
url = f"{self._auth_url}/auth/realms/shinnytech/protocol/openid-connect/token"
121-
self._logger.debug("request token", url=url, params=data, method="POST")
121+
log_data = data.copy()
122+
log_data.pop("password", None)
123+
self._logger.debug("request token", url=url, params=log_data, method="POST")
122124
response = requests.post(url=url, headers=self._base_headers, data=data, timeout=30)
123125
self._logger.debug("request token result", url=response.url, status_code=response.status_code, headers=response.headers, reason=response.reason, text=response.text)
124126
if response.status_code == 200:

tqsdk/backtest/backtest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,14 @@ async def _md_handler(self):
213213
# 收到的 symbols 应该转发给下游
214214
if d.get("symbols"):
215215
self._diffs.append({"symbols": d["symbols"]})
216+
# EDB 指标查询结果:回测模块不理解其数据结构,只做透传,保证下游 api 能收到完成标记
217+
edb_diff = {}
218+
if "_edb_index_data" in d:
219+
edb_diff["_edb_index_data"] = d["_edb_index_data"]
220+
if "_edb_index_data_finished" in d:
221+
edb_diff["_edb_index_data_finished"] = d["_edb_index_data_finished"]
222+
if edb_diff:
223+
self._diffs.append(edb_diff)
216224
# 如果没有收到 quotes(合约信息),或者当前的 self._data.get('quotes', {}) 里没有股票,那么不应该向 _diffs 里添加元素
217225
if recv_quotes:
218226
quotes_stock = self._stock_dividend._get_dividend(self._data.get('quotes', {}), self._trading_day)

tqsdk/channel.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ def __init__(self, api: 'TqApi', last_only: bool = False, logger: Union[Logger,
5151
def _logger_bind(self, **kwargs):
5252
self._logger = self._logger.bind(**kwargs)
5353

54+
def _sanitize_log_item(self, item: Any) -> Any:
55+
"""
56+
Sanitize sensitive fields for channel logging only (do NOT mutate the original item).
57+
58+
Current policy: only sensitive password in req_login packs.
59+
"""
60+
if not self._logger.logger.isEnabledFor(TqChan._level):
61+
return item
62+
if isinstance(item, dict) and item.get("aid") == "req_login":
63+
log_item = item.copy()
64+
log_item.pop("password", None)
65+
return log_item
66+
return item
67+
5468
async def close(self) -> None:
5569
"""
5670
关闭channel
@@ -73,7 +87,7 @@ async def send(self, item: Any) -> None:
7387
while not self.empty():
7488
asyncio.Queue.get_nowait(self)
7589
await asyncio.Queue.put(self, item)
76-
self._logger.log(TqChan._level, "tqchan send", item=item)
90+
self._logger.log(TqChan._level, "tqchan send", item=self._sanitize_log_item(item))
7791

7892
def send_nowait(self, item: Any) -> None:
7993
"""
@@ -90,7 +104,7 @@ def send_nowait(self, item: Any) -> None:
90104
while not self.empty():
91105
asyncio.Queue.get_nowait(self)
92106
asyncio.Queue.put_nowait(self, item)
93-
self._logger.log(TqChan._level, "tqchan send_nowait", item=item)
107+
self._logger.log(TqChan._level, "tqchan send_nowait", item=self._sanitize_log_item(item))
94108

95109
async def recv(self) -> Any:
96110
"""
@@ -102,7 +116,7 @@ async def recv(self) -> Any:
102116
if self._closed and self.empty():
103117
return None
104118
item = await asyncio.Queue.get(self)
105-
self._logger.log(TqChan._level, "tqchan recv", item=item)
119+
self._logger.log(TqChan._level, "tqchan recv", item=self._sanitize_log_item(item))
106120
return item
107121

108122
def recv_nowait(self) -> Any:
@@ -118,7 +132,7 @@ def recv_nowait(self) -> Any:
118132
if self._closed and self.empty():
119133
return None
120134
item = asyncio.Queue.get_nowait(self)
121-
self._logger.log(TqChan._level, "tqchan recv_nowait", item=item)
135+
self._logger.log(TqChan._level, "tqchan recv_nowait", item=self._sanitize_log_item(item))
122136
return item
123137

124138
def recv_latest(self, latest: Any) -> Any:
@@ -133,7 +147,7 @@ def recv_latest(self, latest: Any) -> Any:
133147
"""
134148
while (self._closed and self.qsize() > 1) or (not self._closed and not self.empty()):
135149
latest = asyncio.Queue.get_nowait(self)
136-
self._logger.log(TqChan._level, "tqchan recv_latest", item=latest)
150+
self._logger.log(TqChan._level, "tqchan recv_latest", item=self._sanitize_log_item(latest))
137151
return latest
138152

139153
def __aiter__(self):
@@ -143,7 +157,7 @@ async def __anext__(self):
143157
value = await asyncio.Queue.get(self)
144158
if self._closed and self.empty():
145159
raise StopAsyncIteration
146-
self._logger.log(TqChan._level, "tqchan recv_next", item=value)
160+
self._logger.log(TqChan._level, "tqchan recv_next", item=self._sanitize_log_item(value))
147161
return value
148162

149163
async def __aenter__(self):

tqsdk/connect.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ async def _send_handler(self, send_chan, client):
240240
warnings.warn(f"订阅合约信息字段总长度大于 {self._query_max_length},可能会引起服务器限制。", stacklevel=3)
241241
msg = json.dumps(pack)
242242
await client.send(msg)
243+
if pack.get("aid") == "req_login":
244+
log_pack = pack.copy()
245+
log_pack.pop("password", None)
246+
msg = json.dumps(log_pack)
243247
self._logger.debug("websocket send data", pack=msg)
244248
except asyncio.CancelledError: # 取消任务不抛出异常,不然等待者无法区分是该任务抛出的取消异常还是有人直接取消等待者
245249
pass
@@ -278,7 +282,11 @@ async def _run(self, api, api_send_chan, api_recv_chan, ws_send_chan, ws_recv_ch
278282
for msg in self._resend_request.values():
279283
# 这里必须用 send_nowait 而不是 send,因为如果使用异步写法,在循环中,代码可能执行到 send_task, 可能会修改 _resend_request
280284
ws_send_chan.send_nowait(msg)
281-
self._logger.debug("resend request", pack=msg)
285+
log_msg = msg
286+
if log_msg.get("aid") == "req_login":
287+
log_msg = msg.copy()
288+
log_msg.pop("password", None)
289+
self._logger.debug("resend request", pack=log_msg)
282290
await ws_send_chan.send({"aid": "peek_message"})
283291
elif self._un_processed: # 处理重连后数据
284292
pack_data = pack.get("data", [])

0 commit comments

Comments
 (0)