From 01954142b4fcf6657d384fb16aea8e8ad7cc1248 Mon Sep 17 00:00:00 2001 From: shinny-pack Date: Wed, 29 Sep 2021 09:08:15 +0000 Subject: [PATCH] Update Version 2.9.0 --- PKG-INFO | 2 +- doc/conf.py | 4 ++-- doc/profession.rst | 2 +- doc/reference/tqsdk.lib.rst | 8 +++++++ doc/usage/backtest.rst | 22 +++++++++++++++++ doc/usage/mddatas.rst | 2 +- doc/version.rst | 10 ++++++++ setup.py | 2 +- tqsdk/__version__.py | 2 +- tqsdk/account.py | 31 +++++++++++++----------- tqsdk/api.py | 23 ++++++++++++++---- tqsdk/backtest.py | 1 + tqsdk/baseApi.py | 26 ++++++++++++++++---- tqsdk/connect.py | 13 ++++++++-- tqsdk/data_extension.py | 7 +++--- tqsdk/multiaccount.py | 4 ++-- tqsdk/objs.py | 5 ++++ tqsdk/objs_not_entity.py | 11 +++++---- tqsdk/symbols.py | 2 ++ tqsdk/utils.py | 47 +++++++++++++++---------------------- tqsdk/utils_symbols.py | 2 ++ 21 files changed, 157 insertions(+), 69 deletions(-) diff --git a/PKG-INFO b/PKG-INFO index 4e5116d1..a93712f7 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: tqsdk -Version: 2.8.6 +Version: 2.9.0 Summary: TianQin SDK Home-page: https://www.shinnytech.com/tqsdk Author: TianQin diff --git a/doc/conf.py b/doc/conf.py index fe93a914..76b4bff2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = u'2.8.6' +version = u'2.9.0' # The full version, including alpha/beta/rc tags. -release = u'2.8.6' +release = u'2.9.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/profession.rst b/doc/profession.rst index 6e9ac065..7999fe1d 100644 --- a/doc/profession.rst +++ b/doc/profession.rst @@ -38,7 +38,7 @@ TqSdk 中大部分功能是供用户免费使用的, 同时我们也提供了 Tq ------------------------------------------------- TqSdk 免费版本提供全部的期货、商品/金融期权和上证50、沪深300和中证500的实时行情 -其他股票类行情需购买或申请 TqSdk 专业版试用才可提供,TqSdk 中股票示例代码参考如下:: +购买或申请 TqSdk 专业版试用后可提供A股股票的实时和历史行情,TqSdk 中股票示例代码参考如下:: SSE.600000 - 上交所浦发银行股票编码 SZSE.000001 - 深交所平安银行股票编码 diff --git a/doc/reference/tqsdk.lib.rst b/doc/reference/tqsdk.lib.rst index b90d8cc1..04505b34 100644 --- a/doc/reference/tqsdk.lib.rst +++ b/doc/reference/tqsdk.lib.rst @@ -4,6 +4,14 @@ tqsdk.lib - 业务工具库 ------------------------------------------------------------------ +.. _tqsdk.lib.notify: + +tqsdk.lib.notify - 收集通知信息工具 +================================================================== +.. automodule:: tqsdk.lib.notify + :members: + + .. _tqsdk.lib.target_pos_task: tqsdk.lib.target_pos_task - 目标持仓工具 diff --git a/doc/usage/backtest.rst b/doc/usage/backtest.rst index 067eced0..76e9b6d8 100644 --- a/doc/usage/backtest.rst +++ b/doc/usage/backtest.rst @@ -59,6 +59,28 @@ .. figure:: ../images/web_gui_backtest.png +.. _backtest_with_web_gui: + +回测结束在浏览器中查看绘图结果 +------------------------------------------------- + +要在回测结束时,如果依然需要在浏览器中查看绘图结果,同时又需要打印回测信息,您应该这样做:: + + from tqsdk import BacktestFinished + + acc = TqSim() + + try: + api = TqApi(acc, backtest=TqBacktest(start_dt=date(2018, 5, 1), end_dt=date(2018, 10, 1)), auth=TqAuth("信易账户", "账户密码")) + #策略代码在这里 + #... + except BacktestFinished as e: + print(acc.tqsdk_stat) # 回测时间内账户交易信息统计结果,其中包含以下字段 + # 由于需要在浏览器中查看绘图结果,因此程序不能退出 + while True: + api.wait_update() + + .. _backtest_underlying_symbol: 回测时获取主连合约标的 diff --git a/doc/usage/mddatas.rst b/doc/usage/mddatas.rst index bfe9eac5..1e519885 100644 --- a/doc/usage/mddatas.rst +++ b/doc/usage/mddatas.rst @@ -9,7 +9,7 @@ TqSdk中的合约代码, 统一采用 交易所代码.交易所内品种代码 其中 TqSdk 免费版本提供全部的期货、商品/金融期权和上证50、沪深300和中证500的实时行情 -其他股票类行情需购买或申请 TqSdk 专业版才可提供,具体 TqSdk免费版和专业版的区别,请点击 `天勤量化专业版 `_ +购买或申请 TqSdk 专业版试用后可提供A股股票的实时和历史行情,具体免费版和专业版的区别,请点击 `天勤量化专业版 `_ 目前 TqSdk 支持的交易所包括: diff --git a/doc/version.rst b/doc/version.rst index 5c6f4999..5297b6e6 100644 --- a/doc/version.rst +++ b/doc/version.rst @@ -2,6 +2,16 @@ 版本变更 ============================= +2.9.0 (2021/09/29) + +* 增加::py:meth:`~tqsdk.api.TqApi.query_symbol_info` 接口返回值中增加 ``pre_open_interest``, ``pre_settlement``, ``pre_close`` 这三个字段 +* 优化:重构网络连接,增加多账户测试用例 +* 优化:简化回测结束后用户依然需要查看 web_gui 时的代码,详情参考 :ref:`backtest_with_web_gui` +* 优化:网络连接失败时,优化对用户的提示信息 +* 优化:实盘账户实盘不支持主连和指数交易,提前抛错提示用户 +* docs:更新文档,专业版承诺提供A股股票行情 + + 2.8.6 (2021/09/16) * 增加:TqApi 增加 :py:meth:`~tqsdk.api.TqApi.query_his_cont_quotes` 接口,可以获取过去 n 个交易日的历史主连信息 diff --git a/setup.py b/setup.py index 248b754c..4cafcd64 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ def get_tag(self): setuptools.setup( name='tqsdk', - version="2.8.6", + version="2.9.0", description='TianQin SDK', author='TianQin', author_email='tianqincn@gmail.com', diff --git a/tqsdk/__version__.py b/tqsdk/__version__.py index 7039ccb2..387cfacc 100644 --- a/tqsdk/__version__.py +++ b/tqsdk/__version__.py @@ -1 +1 @@ -__version__ = '2.8.6' +__version__ = '2.9.0' diff --git a/tqsdk/account.py b/tqsdk/account.py index ef6f55f6..38ed4a3c 100644 --- a/tqsdk/account.py +++ b/tqsdk/account.py @@ -106,10 +106,11 @@ async def _run(self, api, api_send_chan, api_recv_chan, md_send_chan, md_recv_ch "aid": "confirm_settlement" }) # 自动发送确认结算单 self._pending_peek = False # 是否有下游收到未处理的 peek_message - self._md_pending_peek = False # 是否有发给上游的 peek_message,未收到过回复 + self._md_pending_peek = False # 是否有发给行情上游的 peek_message,未收到过回复 + self._td_pending_peek = False # 是否有发给交易上游的 peek_message,未收到过回复 self._diffs = [] - md_task = api.create_task(self._md_handler(api_recv_chan, md_send_chan, md_recv_chan)) - td_task = api.create_task(self._td_handler(api_recv_chan, td_send_chan, td_recv_chan)) + md_task = api.create_task(self._md_handler(api_recv_chan, md_recv_chan)) + td_task = api.create_task(self._td_handler(api_recv_chan, td_recv_chan)) try: async for pack in api_send_chan: if pack["aid"] == "subscribe_quote" or pack["aid"] == "set_chart" or pack["aid"] == "ins_query": @@ -125,9 +126,13 @@ async def _run(self, api, api_send_chan, api_recv_chan, md_send_chan, md_recv_ch elif pack["aid"] == "peek_message": self._pending_peek = True await self._send_diff(api_recv_chan) - if self._pending_peek and self._md_pending_peek is False: # 控制"peek_message"发送: 当没有新的事件需要用户处理时才推进到下一个行情 - await md_send_chan.send(pack) - self._md_pending_peek = True + if self._pending_peek: + if self._md_pending_peek is False: # 控制"peek_message"发送: 当没有新的事件需要用户处理时才推进到下一个行情 + await md_send_chan.send({"aid": "peek_message"}) + self._md_pending_peek = True + if self._td_pending_peek is False: # 控制"peek_message"发送: 当没有新的事件需要用户处理时才推进到下一个行情 + await td_send_chan.send({"aid": "peek_message"}) + self._td_pending_peek = True finally: md_task.cancel() td_task.cancel() @@ -142,16 +147,16 @@ async def _send_diff(self, api_recv_chan): self._pending_peek = False await api_recv_chan.send(rtn_data) - async def _md_handler(self, api_recv_chan, md_send_chan, md_recv_chan): + async def _md_handler(self, api_recv_chan, md_recv_chan): async for pack in md_recv_chan: if pack["aid"] == "rtn_data": + self._md_pending_peek = False self._diffs.extend(pack.get('data', [])) + await self._send_diff(api_recv_chan) else: await api_recv_chan.send(pack) # 有可能是另一个 account 的 rsp_login - self._md_pending_peek = False - await self._send_diff(api_recv_chan) - async def _td_handler(self, api_recv_chan, td_send_chan, td_recv_chan): + async def _td_handler(self, api_recv_chan, td_recv_chan): async for pack in td_recv_chan: # OTG 返回业务信息截面 trade 中 account_key 为 user_id, 该值需要替换为 account_key for _, slice_item in enumerate(pack["data"] if "data" in pack else []): @@ -166,13 +171,11 @@ async def _td_handler(self, api_recv_chan, td_send_chan, td_recv_chan): elif self._sub_account_id in slice_item["trade"]: slice_item["trade"][self._account_key] = slice_item["trade"].pop(self._sub_account_id) if pack["aid"] == "rtn_data": + self._td_pending_peek = False self._diffs.extend(pack.get('data', [])) + await self._send_diff(api_recv_chan) else: await api_recv_chan.send(pack) - await td_send_chan.send({ - "aid": "peek_message" - }) - await self._send_diff(api_recv_chan) def _get_sub_account(self, trade): """ 股票账户 diff --git a/tqsdk/api.py b/tqsdk/api.py index f88e6692..893a27ab 100644 --- a/tqsdk/api.py +++ b/tqsdk/api.py @@ -1143,7 +1143,8 @@ def insert_order(self, symbol: str, direction: str, offset: str = "", volume: in """ (exchange_id, instrument_id) = symbol.split(".", 1) - if not self._account._check_valid(account): + account = self._account._check_valid(account) + if account is None: raise Exception(f"多账户模式下, 需要指定账户实例 account") if direction not in ("BUY", "SELL"): raise Exception("下单方向(direction) %s 错误, 请检查 direction 参数是否填写正确" % (direction)) @@ -1152,6 +1153,8 @@ def insert_order(self, symbol: str, direction: str, offset: str = "", volume: in raise Exception("下单数量(volume) %s 错误, 请检查 volume 是否填写正确" % (volume)) if limit_price != limit_price: raise Exception(f"limit_price 参数不支持设置为 {limit_price}。") + if isinstance(account, TqAccount) and exchange_id == "KQ": + raise Exception(f"账户 {account._broker_id}, {account._account_id} 不支持交易合约 {symbol}。") # 股票下单时, 不支持 order_id 和 offset 参数 if exchange_id in ["SSE", "SZSE"] and self._account._is_stock_type(account): if order_id: @@ -1746,7 +1749,7 @@ def wait_update(self, deadline: Optional[float] = None, _task: Union[asyncio.Tas "TqSdk 使用了 python3 的原生协程和异步通讯库 asyncio,您所使用的 IDE 不支持 asyncio, 请使用 pycharm 或其它支持 asyncio 的 IDE") self._wait_timeout = False # 先尝试执行各个task,再请求下个业务数据,可能用户的同步代码会在 chan 中 send 数据,需要先 run_tasks - self._run_until_idle() + self._run_until_idle(async_run=False) # 用户可能在同步或者异步代码中修改 klines 附加列的值 # 同步代码:此次调用 wait_update 之前应该已经修改执行 @@ -1754,7 +1757,7 @@ def wait_update(self, deadline: Optional[float] = None, _task: Union[asyncio.Tas # 所以放在这里处理, 总会发送 serial_extra_array 数据,由 TqWebHelper 处理 for _, serial in self._serials.items(): self._process_serial_extra_array(serial) - self._run_until_idle() # 这里 self._run_until_idle() 主要为了把上一步计算出得需要绘制的数据发送到 TqWebHelper + self._run_until_idle(async_run=False) # 这里 self._run_until_idle() 主要为了把上一步计算出得需要绘制的数据发送到 TqWebHelper if _task is not None: # 如果 _task 已经 done,则提前返回 True, False 代表超时会抛错 _tasks = _task if isinstance(_task, list) else [_task] @@ -2595,6 +2598,9 @@ def query_symbol_info(self, symbol: Union[str, List[str]]): * exercise_year: 期权最后行权日年份,只对期权品种有效。 * exercise_month: 期权最后行权日月份,只对期权品种有效。 * option_class: 期权方向 + * pre_settlement: 昨结算 + * pre_open_interest: 昨持仓 + * pre_close: 昨收盘 Example1:: @@ -2981,7 +2987,14 @@ def _setup_connection(self): # 连接合约和行情服务器 if self._md_url is None: - self._md_url = self._auth._get_md_url(self._stock, backtest=isinstance(self._backtest, TqBacktest)) # 如果用户未指定行情地址,则使用名称服务获取行情地址 + try: + self._md_url = self._auth._get_md_url(self._stock, backtest=isinstance(self._backtest, TqBacktest)) # 如果用户未指定行情地址,则使用名称服务获取行情地址 + except Exception as e: + now = datetime.now() + if now.hour == 19 and 0 <= now.minute <= 30: + raise Exception(f"{e}, 每日 19:00-19:30 为日常运维时间,请稍后再试") + else: + raise md_logger = ShinnyLoggerAdapter(self._logger.getChild("TqConnect"), url=self._md_url) ws_md_send_chan = TqChan(self, chan_name="send to md", logger=md_logger) ws_md_recv_chan = TqChan(self, chan_name="recv from md", logger=md_logger) @@ -3071,9 +3084,9 @@ def _setup_connection(self): data_extension = DataExtension(self) data_extension_send_chan = TqChan(self, chan_name="send to data_extension") data_extension_recv_chan = TqChan(self, chan_name="recv from data_extension") - self.create_task(data_extension._run(data_extension_send_chan, data_extension_recv_chan, self._send_chan, self._recv_chan)) self._send_chan._logger_bind(chan_from="data_extension") self._recv_chan._logger_bind(chan_to="data_extension") + self.create_task(data_extension._run(data_extension_send_chan, data_extension_recv_chan, self._send_chan, self._recv_chan)) self._send_chan, self._recv_chan = data_extension_send_chan, data_extension_recv_chan self._send_chan._logger_bind(chan_from="api") self._recv_chan._logger_bind(chan_to="api") diff --git a/tqsdk/backtest.py b/tqsdk/backtest.py index d5860273..af91bc27 100644 --- a/tqsdk/backtest.py +++ b/tqsdk/backtest.py @@ -336,6 +336,7 @@ async def _send_diff(self): } }] }) + await self._api._wait_until_idle() raise BacktestFinished(self._api) from None for ins, diff in quotes.items(): self._quotes[ins]["sended_init_quote"] = True diff --git a/tqsdk/baseApi.py b/tqsdk/baseApi.py index 406b9acd..99b51f19 100644 --- a/tqsdk/baseApi.py +++ b/tqsdk/baseApi.py @@ -6,6 +6,7 @@ import functools import sys import time +from asyncio import Future from typing import Optional, Coroutine @@ -23,6 +24,7 @@ def __init__(self, loop: Optional[asyncio.AbstractEventLoop] = None) -> None: """ self._loop = asyncio.SelectorEventLoop() if loop is None else loop # 创建一个新的 ioloop, 避免和其他框架/环境产生干扰 self._event_rev, self._check_rev = 0, 0 + self._wait_idle_list = [] # 所有等待 loop idle 的 Future self._wait_timeout = False # wait_update 是否触发超时 self._tasks = set() # 由api维护的所有根task,不包含子task,子task由其父task维护 self._exceptions = [] # 由api维护的所有task抛出的例外 @@ -54,14 +56,27 @@ def _run_once(self): if self._exceptions: raise self._exceptions.pop(0) - def _run_until_idle(self): - """执行 ioloop 直到没有待执行任务""" + def _run_until_idle(self, async_run=False): + """执行 ioloop 直到没有待执行任务 + async_run is True 会从 _wait_idle_list 中取出等待的异步任务,保证同步代码优先于异步代码执行, + 只有被 _run_until_task_done 调用(即 api 等待 fetch_msg)时,async_run 会为 True + """ while self._check_rev != self._event_rev: check_handle = self._loop.call_soon(self._check_event, self._event_rev + 1) try: self._run_once() finally: check_handle.cancel() + if len(self._wait_idle_list) > 0 and async_run: + f = self._wait_idle_list.pop(0) # 取出 list 中的第一个 Future + f.set_result(None) # f 返回 + + async def _wait_until_idle(self): + """等待 ioloop 执行到空闲时,才从网络连接处收数据包,在 TqConnect 类中使用""" + f = Future() + self._wait_idle_list.append(f) + self._loop.stop() + await f def _run_until_task_done(self, task: asyncio.Task, deadline=None): try: @@ -69,7 +84,10 @@ def _run_until_task_done(self, task: asyncio.Task, deadline=None): if deadline is not None: deadline_handle = self._loop.call_later(max(0, deadline - time.time()), self._set_wait_timeout) while not self._wait_timeout and not task.done(): - self._run_once() + if len(self._wait_idle_list) == 0: + self._run_once() + else: + self._run_until_idle(async_run=True) finally: if deadline is not None: deadline_handle.cancel() @@ -101,7 +119,7 @@ async def _windows_patch(self): await asyncio.sleep(1) def _close(self) -> None: - self._run_until_idle() # 由于有的处于 ready 状态 task 可能需要报撤单, 因此一直运行到没有 ready 状态的 task + self._run_until_idle(async_run=False) # 由于有的处于 ready 状态 task 可能需要报撤单, 因此一直运行到没有 ready 状态的 task for task in self._tasks: task.cancel() while self._tasks: # 等待 task 执行完成 diff --git a/tqsdk/connect.py b/tqsdk/connect.py index a8857be1..94f54c33 100644 --- a/tqsdk/connect.py +++ b/tqsdk/connect.py @@ -10,6 +10,8 @@ import time import warnings from abc import abstractmethod +from asyncio import Future +from datetime import datetime from queue import Queue import certifi @@ -150,6 +152,7 @@ async def _run(self, api, url, send_chan, recv_chan): else: self._logger.debug("websocket connected") # 发送网络连接建立的通知,code = 2019112901 or 2019112902,这里区分了第一次连接和重连 + await self._api._wait_until_idle() await recv_chan.send({ "aid": "rtn_data", "data": [{ @@ -163,8 +166,9 @@ async def _run(self, api, url, send_chan, recv_chan): send_task = self._api.create_task(self._send_handler(send_chan, client)) try: async for msg in client: - self._logger.debug("websocket received data", pack=msg) pack = json.loads(msg) + await self._api._wait_until_idle() + self._logger.debug("websocket received data", pack=msg) await recv_chan.send(pack) finally: if client.reader._start_read_message: @@ -177,15 +181,18 @@ async def _run(self, api, url, send_chan, recv_chan): except (websockets.exceptions.ConnectionClosed, websockets.exceptions.InvalidStatusCode, websockets.exceptions.InvalidState, websockets.exceptions.ProtocolError, OSError, TqBacktestPermissionError) as e: + in_ops_time = datetime.now().hour == 19 and 0 <= datetime.now().minute <= 30 # 发送网络连接断开的通知,code = 2019112911 notify_id = _generate_uuid() notify = { "type": "MESSAGE", "level": "WARNING", "code": 2019112911, - "content": "与 %s 的网络连接断开,请检查客户端及网络是否正常" % url, + "content": f"与 {url} 的网络连接断开,请检查客户端及网络是否正常", "url": url } + if in_ops_time: + notify['content'] += ',每日 19:00-19:30 为日常运维时间,请稍后再试' self._logger.debug("websocket connection closed", error=str(e)) await recv_chan.send({ "aid": "rtn_data", @@ -198,6 +205,8 @@ async def _run(self, api, url, send_chan, recv_chan): if isinstance(e, TqBacktestPermissionError): # 如果错误类型是用户无回测权限,直接返回 raise + if self._first_connect and in_ops_time: + raise Exception(f'与 {url} 的连接失败,每日 19:00-19:30 为日常运维时间,请稍后再试') finally: if self._first_connect: self._first_connect = False diff --git a/tqsdk/data_extension.py b/tqsdk/data_extension.py index 5910d281..c3a7edc9 100644 --- a/tqsdk/data_extension.py +++ b/tqsdk/data_extension.py @@ -81,9 +81,10 @@ async def _run(self, api_send_chan, api_recv_chan, md_send_chan, md_recv_chan): try: async for pack in api_send_chan: if "_md_recv" in pack: - self._pending_peek_md = False - await self._md_recv(pack) - await self._send_diff() + if pack['aid'] == 'rtn_data': + self._pending_peek_md = False + await self._md_recv(pack) + await self._send_diff() if self._pending_peek and self._pending_peek_md is False: self._pending_peek_md = True await self._md_send_chan.send({"aid": "peek_message"}) diff --git a/tqsdk/multiaccount.py b/tqsdk/multiaccount.py index 71f0217d..e0f4f70f 100644 --- a/tqsdk/multiaccount.py +++ b/tqsdk/multiaccount.py @@ -208,8 +208,8 @@ def _connect_td(self, api, account: Union[TqAccount, TqKq] = None, index: int = conn = TqConnect(td_logger) api.create_task(conn._run(api, account._td_url, ws_td_send_chan, ws_td_recv_chan)) - ws_td_send_chan._logger_bind(chan_from="td_reconn") - ws_td_recv_chan._logger_bind(chan_to="td_reconn") + ws_td_send_chan._logger_bind(chan_from=f"td_reconn_{index}") + ws_td_recv_chan._logger_bind(chan_to=f"td_reconn_{index}") td_handler_logger = self._format_logger("TdReconnect", account) td_reconnect = TdReconnectHandler(td_handler_logger) diff --git a/tqsdk/objs.py b/tqsdk/objs.py index 0ce26008..a1d3a2a2 100644 --- a/tqsdk/objs.py +++ b/tqsdk/objs.py @@ -181,6 +181,11 @@ def _instance_entity(self, path): @property def underlying_quote(self): + """ + 标的合约 underlying_symbol 所指定的合约对象,若没有标的合约则为 None + + :return: 标的指定的 :py:class:`~tqsdk.objs.Quote` 对象 + """ if self.underlying_symbol: return _get_obj(self._api._data, ["quotes", self.underlying_symbol], self._api._prototype["quotes"]["#"]) return None diff --git a/tqsdk/objs_not_entity.py b/tqsdk/objs_not_entity.py index b81588b1..4ed31ee9 100644 --- a/tqsdk/objs_not_entity.py +++ b/tqsdk/objs_not_entity.py @@ -11,7 +11,7 @@ from tqsdk.objs import Quote from tqsdk.diff import _get_obj -from tqsdk.utils import _query_for_quote, query_all_fields, _generate_uuid +from tqsdk.utils import _query_for_quote, query_all_fields, _generate_uuid, fragments from tqsdk.tafunc import _get_t_series, get_impv, _get_d1, get_delta, get_theta, get_gamma, get_vega, get_rho """ @@ -228,7 +228,10 @@ def __init__(self, api, symbol_list, backtest_timestamp, *args, **kwargs): "last_exercise_datetime", "exercise_year", "exercise_month", - "option_class" + "option_class", + "pre_settlement", + "pre_open_interest", + "pre_close" ] default_quote = Quote(None) data = [{k: (s if k == "instrument_id" else default_quote[k]) for k in self.__dict__["_columns"]} for s in symbol_list] @@ -241,10 +244,10 @@ async def async_update(self): if self.__dict__["_backtest_timestamp"]: variables["timestamp"] = self.__dict__["_backtest_timestamp"] query = "query ($instrument_id:[String],$timestamp:Int64) {" - query += "multi_symbol_info(instrument_id:$instrument_id,timestamp:$timestamp) {" + query_all_fields + "}}" + query += "multi_symbol_info(instrument_id:$instrument_id,timestamp:$timestamp) {" + query_all_fields + "}}" + fragments else: query = "query ($instrument_id:[String]) {" - query += "multi_symbol_info(instrument_id:$instrument_id) {" + query_all_fields + "}}" + query += "multi_symbol_info(instrument_id:$instrument_id) {" + query_all_fields + "}}" + fragments self.__dict__["_api"]._send_pack({ "aid": "ins_query", "query_id": query_id, diff --git a/tqsdk/symbols.py b/tqsdk/symbols.py index 855c5ceb..16c72ab8 100644 --- a/tqsdk/symbols.py +++ b/tqsdk/symbols.py @@ -21,6 +21,8 @@ async def _run(self, api, sim_send_chan, sim_recv_chan, md_send_chan, md_recv_ch self._md_recv_chan = md_recv_chan self._quotes_all_keys = set(Quote(None).keys()) self._quotes_all_keys = self._quotes_all_keys.union({'margin', 'commission'}) + # 以下字段合约服务也会请求,但是不应该记在 quotes 中,quotes 中的这些字段应该有行情服务负责 + self._quotes_all_keys.difference_update({'pre_open_interest', 'pre_settlement', 'pre_close'}) sim_task = self._api.create_task(self._sim_handler()) try: async for pack in self._md_recv_chan: diff --git a/tqsdk/utils.py b/tqsdk/utils.py index d318f88e..2508fb7f 100644 --- a/tqsdk/utils.py +++ b/tqsdk/utils.py @@ -17,33 +17,24 @@ def _generate_uuid(prefix=''): return f"{prefix + '_' if prefix else ''}{RD.getrandbits(128):032x}" -query_all_fields = """ - ... on basic{ class trading_time{day night} trading_day instrument_id instrument_name price_tick price_decs exchange_id english_name} - ... on stock{ stock_dividend_ratio cash_dividend_ratio} - ... on fund{ cash_dividend_ratio} - ... on bond{ maturity_datetime } - ... on tradeable{ volume_multiple quote_multiple} - ... on index{ index_multiple} - ... on securities{ currency face_value first_trading_datetime buy_volume_unit sell_volume_unit status public_float_share_quantity} - ... on future{ expired product_id product_short_name delivery_year delivery_month expire_datetime settlement_price max_market_order_volume max_limit_order_volume margin commission mmsa} - ... on option{ expired product_short_name expire_datetime last_exercise_datetime settlement_price max_market_order_volume max_limit_order_volume strike_price call_or_put exercise_type} - ... on combine{ expired product_id expire_datetime max_market_order_volume max_limit_order_volume leg1{ ... on basic{instrument_id}} leg2{ ... on basic{instrument_id}} } - ... on derivative{ - underlying{ - count edges{ underlying_multiple node{ - ... on basic{ class trading_time{day night} trading_day instrument_id instrument_name price_tick price_decs exchange_id english_name } - ... on stock{ stock_dividend_ratio cash_dividend_ratio } - ... on fund{ cash_dividend_ratio } - ... on bond{ maturity_datetime } - ... on tradeable{ volume_multiple quote_multiple} - ... on index{ index_multiple} - ... on securities{ currency face_value first_trading_datetime buy_volume_unit sell_volume_unit public_float_share_quantity } - ... on future{ expired product_id product_short_name delivery_year delivery_month expire_datetime settlement_price max_market_order_volume max_limit_order_volume margin commission mmsa} - } - } - } +fragments = """\ +fragment basic on basic {class trading_time{day night} trading_day instrument_id instrument_name price_tick price_decs exchange_id english_name} +fragment stock on stock{ stock_dividend_ratio cash_dividend_ratio} +fragment fund on fund{ cash_dividend_ratio} +fragment bond on bond{ maturity_datetime } +fragment tradeable on tradeable{ pre_close volume_multiple quote_multiple} +fragment index on index{ index_multiple} +fragment securities on securities{ currency face_value first_trading_datetime buy_volume_unit sell_volume_unit status public_float_share_quantity} +fragment future on future{ pre_open_interest expired product_id product_short_name delivery_year delivery_month expire_datetime settlement_price max_market_order_volume max_limit_order_volume margin commission mmsa} +fragment option on option{ pre_open_interest expired product_short_name expire_datetime last_exercise_datetime settlement_price max_market_order_volume max_limit_order_volume strike_price call_or_put exercise_type} +fragment combine on combine{ expired product_id expire_datetime max_market_order_volume max_limit_order_volume leg1{ ...on basic{instrument_id}} leg2{ ...on basic{instrument_id}} } +fragment derivative on derivative{ + underlying{ + count edges{ underlying_multiple node{...basic...stock...fund...bond...tradeable...index...securities...future}} } -""" +}""" + +query_all_fields = "...basic...stock...fund...bond...tradeable...index...securities...future...option...combine...derivative" def _query_for_quote(symbol): @@ -56,7 +47,7 @@ def _query_for_quote(symbol): if any([s == "" for s in symbol_list]) or len(symbol_list) == 0: raise Exception("发送的 ins_query 请求合约代码不支持空字符串、空列表或者列表中包括空字符串。") query = "query ($instrument_id:[String]) {" - query += "multi_symbol_info(instrument_id:$instrument_id) {" + query_all_fields + "}}" + query += "multi_symbol_info(instrument_id:$instrument_id) {" + query_all_fields + "}}" + fragments return { "aid": "ins_query", "query_id": _generate_uuid(prefix='PYSDK_quote_'), @@ -86,7 +77,7 @@ def _query_for_init(): todo: 为了兼容旧版提供给用户的 api._data["quote"].items() 类似用法,应该限制交易所 ["SHFE", "DCE", "CZCE", "INE", "CFFEX", "KQ"] """ query = "query ($class_list:[Class], $exchange_list:[String]) {" - query += "multi_symbol_info(class:$class_list, exchange_id:$exchange_list) {" + query_all_fields + "}}" + query += "multi_symbol_info(class:$class_list, exchange_id:$exchange_list) {" + query_all_fields + "}}" + fragments return query, { "class_list": ["FUTURE", "INDEX", "OPTION", "COMBINE", "CONT"], "exchange_list": ["SHFE", "DCE", "CZCE", "INE", "CFFEX", "KQ"] diff --git a/tqsdk/utils_symbols.py b/tqsdk/utils_symbols.py index b3c1e730..daae268d 100644 --- a/tqsdk/utils_symbols.py +++ b/tqsdk/utils_symbols.py @@ -64,6 +64,8 @@ def _convert_symbol_to_quote(symbol, keys): quote["exercise_year"] = datetime.fromtimestamp(symbol["last_exercise_datetime"] / 1e9).year elif key == "exercise_month" and symbol.get("last_exercise_datetime"): quote["exercise_month"] = datetime.fromtimestamp(symbol["last_exercise_datetime"] / 1e9).month + elif key == "pre_settlement" and "settlement_price" in symbol: + quote["pre_settlement"] = symbol["settlement_price"] elif key in symbol: quote[key] = symbol[key] return quote \ No newline at end of file