Skip to content

Commit

Permalink
Update Version 2.9.0
Browse files Browse the repository at this point in the history
  • Loading branch information
shinny-pack authored and shinny-mayanqiong committed Sep 29, 2021
1 parent eda74de commit 0195414
Show file tree
Hide file tree
Showing 21 changed files with 157 additions and 69 deletions.
2 changes: 1 addition & 1 deletion PKG-INFO
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion doc/profession.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ TqSdk 中大部分功能是供用户免费使用的, 同时我们也提供了 Tq
-------------------------------------------------
TqSdk 免费版本提供全部的期货、商品/金融期权和上证50、沪深300和中证500的实时行情

其他股票类行情需购买或申请 TqSdk 专业版试用才可提供,TqSdk 中股票示例代码参考如下::
购买或申请 TqSdk 专业版试用后可提供A股股票的实时和历史行情,TqSdk 中股票示例代码参考如下::

SSE.600000 - 上交所浦发银行股票编码
SZSE.000001 - 深交所平安银行股票编码
Expand Down
8 changes: 8 additions & 0 deletions doc/reference/tqsdk.lib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 - 目标持仓工具
Expand Down
22 changes: 22 additions & 0 deletions doc/usage/backtest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

回测时获取主连合约标的
Expand Down
2 changes: 1 addition & 1 deletion doc/usage/mddatas.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ TqSdk中的合约代码, 统一采用 交易所代码.交易所内品种代码

其中 TqSdk 免费版本提供全部的期货、商品/金融期权和上证50、沪深300和中证500的实时行情

其他股票类行情需购买或申请 TqSdk 专业版才可提供,具体 TqSdk免费版和专业版的区别,请点击 `天勤量化专业版 <https://www.shinnytech.com/tqsdk_professional/>`_
购买或申请 TqSdk 专业版试用后可提供A股股票的实时和历史行情,具体免费版和专业版的区别,请点击 `天勤量化专业版 <https://www.shinnytech.com/tqsdk_professional/>`_

目前 TqSdk 支持的交易所包括:

Expand Down
10 changes: 10 additions & 0 deletions doc/version.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 个交易日的历史主连信息
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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='[email protected]',
Expand Down
2 changes: 1 addition & 1 deletion tqsdk/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '2.8.6'
__version__ = '2.9.0'
31 changes: 17 additions & 14 deletions tqsdk/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand All @@ -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()
Expand All @@ -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 []):
Expand All @@ -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):
""" 股票账户
Expand Down
23 changes: 18 additions & 5 deletions tqsdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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:
Expand Down Expand Up @@ -1746,15 +1749,15 @@ 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 之前应该已经修改执行
# 异步代码:上一行 self._run_until_idle() 可能会修改 klines 附加列的值
# 所以放在这里处理, 总会发送 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]
Expand Down Expand Up @@ -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::
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions tqsdk/backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 22 additions & 4 deletions tqsdk/baseApi.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import functools
import sys
import time
from asyncio import Future
from typing import Optional, Coroutine


Expand All @@ -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抛出的例外
Expand Down Expand Up @@ -54,22 +56,38 @@ 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:
self._wait_timeout = False
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()
Expand Down Expand Up @@ -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 执行完成
Expand Down
13 changes: 11 additions & 2 deletions tqsdk/connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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": [{
Expand All @@ -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:
Expand All @@ -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",
Expand All @@ -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
Expand Down
Loading

0 comments on commit 0195414

Please sign in to comment.