Skip to content

Commit 2b95ea6

Browse files
shinny-hongyanshinny-chenli
authored andcommitted
Update Version 3.8.0
1 parent 99e6aa2 commit 2b95ea6

22 files changed

+227
-43
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.7.9
3+
Version: 3.8.0
44
Summary: TianQin SDK
55
Home-page: https://www.shinnytech.com/tqsdk
66
Author: TianQin

doc/advanced/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@
1717
unanttended.rst
1818
targetpostask2.rst
1919
scheduler.rst
20+
tq_trading_unit.rst

doc/advanced/tq_trading_unit.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
.. _tq_trading_unit:
2+
3+
============================
4+
tqsdk 多策略使用手册
5+
============================
6+
7+
概述
8+
====
9+
`tqsdk` 提供了 `TqTradingUnit` 账户类型,支持一个实盘账号在多个策略中交易,每个策略交易数据相互隔离
10+
11+
系统配置要求
12+
============
13+
- **Windows**: >= Windows 10
14+
- **Linux**: >= Ubuntu 22.04
15+
16+
安装
17+
====
18+
使用 `TqTradingUnit` 账户类型需要安装 `tqsdk-zq` 包,用来初始化本地环境::
19+
20+
pip install tqsdk-zq
21+
22+
对于 Windows 用户,还需要安装 Microsoft Visual C++ Redistributable,下载地址:
23+
`Microsoft Redistributable <https://aka.ms/vs/17/release/vc_redist.x64.exe>`_
24+
25+
使用流程
26+
========
27+
28+
1. **初始化本地环境**::
29+
30+
tqsdk-zq init --kq-name=xx --kq-password=xx --web-port=xx
31+
32+
- 初始化完成后,控制台会输出多策略管理页的账户、密码以及网址
33+
- 打开浏览器,访问控制台输出的网址
34+
35+
2. **打印多策略控制台地址**::
36+
37+
tqsdk-zq web
38+
39+
- 如果机器重启或者网页打不开了,请执行以上命令
40+
- 进程重新拉起后,控制台会输出管理页网址
41+
42+
3. **访问多策略管理页**:
43+
44+
- 在多策略管理页添加策略组
45+
- 在策略组中添加后端账号
46+
- 在策略组中添加前端号并入金
47+
48+
4. **使用 TqTradingUnit 登录前端账户**::
49+
50+
from tqsdk import TqApi, TqTradingUnit, TqAuth
51+
52+
account = TqTradingUnit(account_id="前端账户", tags=["铜品种策略", "套利策略"])
53+
api = TqApi(account, auth=TqAuth("快期账户", "账户密码"))
54+

doc/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@
4848
# built documents.
4949
#
5050
# The short X.Y version.
51-
version = u'3.7.9'
51+
version = u'3.8.0'
5252
# The full version, including alpha/beta/rc tags.
53-
release = u'3.7.9'
53+
release = u'3.8.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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
版本变更
44
=============================
5+
3.8.0 (2025/04/02)
6+
7+
* 新增: :py:class:`~tqsdk.TqTradingUnit` 账户类型,支持多策略交易,详情参考 :ref:`tq_trading_unit`
8+
* 修复: numpy 移除 NaN 常量导致的指标函数执行失败
9+
* 修复: TqAuth 参数类型错误
10+
* docs: 修正部分文档
11+
12+
513
3.7.9 (2025/03/07)
614

715
* docs: 修正天勤 AI 的指向地址

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.7.9",
11+
version="3.8.0",
1212
description='TianQin SDK',
1313
author='TianQin',
1414
author_email='[email protected]',

tqsdk/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
name = "tqsdk"
55

66
from tqsdk.api import TqApi
7-
from tqsdk.tradeable import TqAccount, TqZq, TqKq, TqKqStock, TqSim, TqSimStock, TqCtp, TqRohon, TqJees, TqYida
7+
from tqsdk.tradeable import TqAccount, TqZq, TqKq, TqKqStock, TqSim, TqSimStock, TqCtp, TqRohon, TqJees, TqYida, TqTradingUnit
88
from tqsdk.auth import TqAuth
99
from tqsdk.channel import TqChan
1010
from tqsdk.backtest import TqBacktest, TqReplay

tqsdk/__pyinstaller/hook-tqsdk.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
from PyInstaller.utils.hooks import collect_data_files
2-
datas = collect_data_files('tqsdk', includes=['ctpse', 'web', 'expired_quotes.json.lzma'])
1+
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
2+
datas = collect_data_files('tqsdk', includes=['web', 'expired_quotes.json.lzma'])
3+
hiddenimports = collect_submodules('tqsdk_ctpse')

tqsdk/__version__.py

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

tqsdk/api.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@
1010
* 提供本地的模拟交易账户,同时完成撮合成交
1111
* 支持回测功能
1212
13-
1413
* PYTHON SDK使用文档: https://doc.shinnytech.com/pysdk/latest/
15-
* 天勤vscode插件使用文档: https://doc.shinnytech.com/pysdk/latest/devtools/vscode.html
16-
* 天勤用户论坛: https://www.shinnytech.com/qa/
1714
"""
1815
__author__ = 'chengzhi'
1916

tqsdk/auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
class TqAuth(object):
1919
"""信易用户认证类"""
2020

21-
def __init__(self, user_name: str = "", password: str = ""):
21+
def __init__(self, user_name: str, password: str):
2222
"""
2323
创建快期用户认证类
2424

tqsdk/connect.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import base64
1212
from abc import abstractmethod
1313
from logging import Logger
14-
from queue import Queue
1514
from typing import Optional
1615
from urllib.parse import urlparse
1716

@@ -24,10 +23,11 @@
2423
from tqsdk.datetime import _cst_now
2524
from tqsdk.diff import _merge_diff, _get_obj
2625
from tqsdk.entity import Entity
27-
from tqsdk.exceptions import TqBacktestPermissionError
26+
from tqsdk.exceptions import TqBacktestPermissionError, TqContextManagerError
2827
from tqsdk.utils import _generate_uuid
2928
from tqsdk.sm import SMContext, NullContext
3029
from tqsdk.zq_otg import ZqOtgContext
30+
from tqsdk.zq import ZqContext
3131

3232
"""
3333
优化代码结构,修改为
@@ -94,8 +94,8 @@ async def _run(self, api, url, send_chan, recv_chan):
9494
# 调整代码位置,方便 monkey patch
9595
self._query_max_length = 50000 # ins_query 最大长度
9696
self._ins_list_max_length = 100000 # subscribe_quote 最大长度
97-
self._subscribed_per_seconds = 100 # 每秒 subscribe_quote 请求次数限制
98-
self._subscribed_queue = Queue(self._subscribed_per_seconds)
97+
self._subscribed_ins_list_throttle = 100 # 超过该阈值的订阅请求将进行流控检查
98+
self._subscribed_counts = 0
9999

100100
# websockets 14.0版本升级后用法有变化
101101
if websocket_version_ge_14:
@@ -114,18 +114,18 @@ async def _run(self, api, url, send_chan, recv_chan):
114114
elif url_info.scheme.startswith("sm"):
115115
sm_info = url_info.path.split("/", 4)
116116
cm = SMContext(self._logger, self._api, url_info.scheme, sm_info[1], base64.urlsafe_b64decode(sm_info[2]).decode("utf-8"), base64.urlsafe_b64decode(sm_info[3]).decode("utf-8"))
117-
url_info = url_info._replace(scheme="ws", path="/".join(sm_info[:1]+sm_info[4:]))
117+
url_info = url_info._replace(path="/".join(sm_info[:1]+sm_info[4:]))
118118
elif url_info.scheme.startswith("zqotg"):
119-
url_info = url_info._replace(scheme="ws")
120119
cm = ZqOtgContext(self._api)
120+
elif url_info.scheme.startswith("zq"):
121+
cm = ZqContext(self._api)
121122

122123
count = 0
123124
async with cm:
124125
while True:
125126
try:
126-
if isinstance(cm, (SMContext, ZqOtgContext)):
127-
addr = await cm.get_addr()
128-
url = url_info._replace(netloc=addr).geturl()
127+
if isinstance(cm, (SMContext, ZqOtgContext, ZqContext)):
128+
url = await cm.get_url(url_info)
129129
if not self._first_connect:
130130
notify_id = _generate_uuid()
131131
notify = {
@@ -187,7 +187,7 @@ async def _run(self, api, url, send_chan, recv_chan):
187187
await self._api._cancel_task(send_task)
188188
# 希望做到的效果是遇到网络问题可以断线重连, 但是可能抛出的例外太多了(TimeoutError,socket.gaierror等), 又没有文档或工具可以理出 try 代码中所有可能遇到的例外
189189
# 而这里的 except 又需要处理所有子函数及子函数的子函数等等可能抛出的例外, 因此这里只能遇到问题之后再补, 并且无法避免 false positive 和 false negative
190-
except websocket_expect_exc + (asyncio.TimeoutError, OSError, EOFError, TqBacktestPermissionError) as e:
190+
except websocket_expect_exc + (asyncio.TimeoutError, OSError, EOFError, TqBacktestPermissionError, TqContextManagerError) as e:
191191
in_ops_time = _cst_now().hour == 19 and 0 <= _cst_now().minute <= 30
192192
# 发送网络连接断开的通知,code = 2019112911
193193
notify_id = _generate_uuid()
@@ -231,11 +231,10 @@ async def _send_handler(self, send_chan, client):
231231
if pack.get("aid") == "subscribe_quote":
232232
if len(pack.get("ins_list", "")) > self._ins_list_max_length:
233233
warnings.warn(f"订阅合约字符串总长度大于 {self._ins_list_max_length},可能会引起服务器限制。", stacklevel=3)
234-
if self._subscribed_queue.full():
235-
first_time = self._subscribed_queue.get()
236-
if time.time() - first_time < 1:
237-
warnings.warn(f"1s 内订阅请求次数超过 {self._subscribed_per_seconds} 次,订阅多合约时推荐使用 api.get_quote_list 方法。", stacklevel=3)
238-
self._subscribed_queue.put(time.time())
234+
ins_list_counts = len(pack.get("ins_list", "").split(","))
235+
self._subscribed_counts += 1
236+
if ins_list_counts > self._subscribed_ins_list_throttle and ins_list_counts / self._subscribed_counts < 2:
237+
warnings.warn("订阅多合约时推荐使用 api.get_quote_list 方法批量订阅。", stacklevel=3)
239238
if pack.get("aid") == "ins_query":
240239
if len(pack.get("query", "")) > self._query_max_length:
241240
warnings.warn(f"订阅合约信息字段总长度大于 {self._query_max_length},可能会引起服务器限制。", stacklevel=3)

tqsdk/exceptions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,14 @@ class TqRiskRuleError(Exception):
115115
def __init__(self, message):
116116
super().__init__(message)
117117
self.message = message
118+
119+
120+
class TqContextManagerError(Exception):
121+
"""
122+
连接管理器报错
123+
"""
124+
125+
def __init__(self, message):
126+
super().__init__(message)
127+
self.message = message
128+

tqsdk/lib/target_pos_task.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ async def demo(SYMBOL):
479479
480480
symbol_list = ["SHFE.rb2107", "DCE.m2109"] # 设置合约代码
481481
for symbol in symbol_list:
482-
api.create_task(demo("SHFE.rb2107")) # 为每个合约创建异步任务
482+
api.create_task(demo(symbol)) # 为每个合约创建异步任务
483483
484484
while True:
485485
api.wait_update()

tqsdk/multiaccount.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from shinny_structlog import ShinnyLoggerAdapter
99

1010
from tqsdk.channel import TqChan
11-
from tqsdk.tradeable import TqAccount, TqKq, TqKqStock, TqSim, TqSimStock, BaseSim, TqZq, TqCtp, TqRohon, TqJees, TqYida
11+
from tqsdk.tradeable import TqAccount, TqKq, TqKqStock, TqSim, TqSimStock, BaseSim, TqZq, TqCtp, TqRohon, TqJees, TqYida, TqTradingUnit
1212
from tqsdk.tradeable.mixin import StockMixin
1313

1414

@@ -27,12 +27,12 @@ class TqMultiAccount(object):
2727
2828
"""
2929

30-
def __init__(self, accounts: Optional[List[Union[TqAccount, TqKq, TqZq, TqKqStock, TqSim, TqSimStock, TqZq, TqCtp, TqRohon, TqJees, TqYida]]] = None):
30+
def __init__(self, accounts: Optional[List[Union[TqAccount, TqKq, TqZq, TqKqStock, TqSim, TqSimStock, TqZq, TqCtp, TqRohon, TqJees, TqYida, TqTradingUnit]]] = None):
3131
"""
3232
创建 TqMultiAccount 实例
3333
3434
Args:
35-
accounts (List[Union[TqAccount, TqKq, TqKqStock, TqSim, TqSimStock, TqZq, TqCtp, TqRohon, TqJees, TqYida]]): [可选] 多账户列表, 若未指定任何账户, 则为 [TqSim()]
35+
accounts (List[Union[TqAccount, TqKq, TqKqStock, TqSim, TqSimStock, TqZq, TqCtp, TqRohon, TqJees, TqYida, TqTradingUnit]]): [可选] 多账户列表, 若未指定任何账户, 则为 [TqSim()]
3636
3737
Example1::
3838

tqsdk/sm.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pathlib import Path
1313
from asyncio.subprocess import PIPE
1414

15+
from tqsdk.exceptions import TqContextManagerError
1516
from tqsdk_sm import get_sm_path
1617

1718

@@ -108,8 +109,8 @@ async def __aenter__(self):
108109
self._sm_cfg = {**json.loads(decrypt_out), **self._sm_cfg}
109110
return self
110111

111-
async def get_addr(self):
112-
"""无法启动时返回空字符串"""
112+
async def get_url(self, url_info):
113+
"""无法启动时抛出 TqContextManagerError 例外"""
113114
if sys.platform.startswith("win"):
114115
# subprocess.Popen 需要调用 poll 才会更新 returncode
115116
self._sm_proc.poll()
@@ -130,7 +131,9 @@ async def get_addr(self):
130131
self._sm_proc.stdin.write(json.dumps(self._sm_init).encode("utf-8"))
131132
self._sm_proc.stdin.write(json.dumps(self._sm_cfg).encode("utf-8"))
132133
self._sm_addr = (await self._sm_proc.stdout.readline()).decode("utf-8").strip()
133-
return self._sm_addr
134+
if not self._sm_addr:
135+
raise TqContextManagerError("获取国密服务地址失败")
136+
return url_info._replace(scheme="ws", netloc=self._sm_addr).geturl()
134137

135138
async def __aexit__(self, exc_type, exc, tb):
136139
try:

tqsdk/ta.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,8 @@ def DMI(df, n, m):
170170
ld = pre_low - df["low"]
171171
admp = tqsdk.tafunc.ma(pd.Series(np.where((hd > 0) & (hd > ld), hd, 0)), n)
172172
admm = tqsdk.tafunc.ma(pd.Series(np.where((ld > 0) & (ld > hd), ld, 0)), n)
173-
new_df["pdi"] = pd.Series(np.where(new_df["atr"] > 0, admp / new_df["atr"] * 100, np.NaN)).ffill()
174-
new_df["mdi"] = pd.Series(np.where(new_df["atr"] > 0, admm / new_df["atr"] * 100, np.NaN)).ffill()
173+
new_df["pdi"] = pd.Series(np.where(new_df["atr"] > 0, admp / new_df["atr"] * 100, np.nan)).ffill()
174+
new_df["mdi"] = pd.Series(np.where(new_df["atr"] > 0, admm / new_df["atr"] * 100, np.nan)).ffill()
175175
ad = pd.Series(np.absolute(new_df["mdi"] - new_df["pdi"]) / (new_df["mdi"] + new_df["pdi"]) * 100)
176176
new_df["adx"] = tqsdk.tafunc.ma(ad, m)
177177
new_df["adxr"] = (new_df["adx"] + new_df["adx"].shift(m)) / 2
@@ -271,7 +271,7 @@ def MACD(df, short, long, m):
271271
def _sar(open, high, low, close, range_high, range_low, n, step, maximum):
272272
n = max(np.sum(np.isnan(range_high)), np.sum(np.isnan(range_low))) + 2
273273
sar = np.empty_like(close)
274-
sar[:n] = np.NAN
274+
sar[:n] = np.nan
275275
af = 0
276276
ep = 0
277277
trend = 1 if (close[n] - open[n]) > 0 else -1

tqsdk/tradeable/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55

66

77
from tqsdk.tradeable.otg.base_otg import BaseOtg
8-
from tqsdk.tradeable.otg import TqAccount, TqZq, TqKq, TqKqStock, TqCtp, TqRohon, TqJees, TqYida
8+
from tqsdk.tradeable.otg import TqAccount, TqZq, TqKq, TqKqStock, TqCtp, TqRohon, TqJees, TqYida, TqTradingUnit
99
from tqsdk.tradeable.sim.basesim import BaseSim
1010
from tqsdk.tradeable.sim import TqSim, TqSimStock

tqsdk/tradeable/otg/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
from tqsdk.tradeable.otg.tqrohon import TqRohon
1111
from tqsdk.tradeable.otg.tqjees import TqJees
1212
from tqsdk.tradeable.otg.tqyida import TqYida
13+
from tqsdk.tradeable.otg.tqtradingunit import TqTradingUnit

tqsdk/tradeable/otg/tqtradingunit.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!usr/bin/env python3
2+
# -*- coding:utf-8 -*-
3+
__author__ = 'chenli'
4+
5+
import hashlib
6+
from typing import Optional, List
7+
import inspect
8+
9+
from tqsdk.tradeable.otg.base_otg import BaseOtg
10+
from tqsdk.tradeable.mixin import FutureMixin
11+
12+
13+
class TqTradingUnit(BaseOtg, FutureMixin):
14+
"""交易单元类"""
15+
16+
def __init__(self, account_id: str, tags: Optional[List[str]]=None) -> None:
17+
"""
18+
创建交易单元实例
19+
20+
Args:
21+
account_id (str): 众期子账户
22+
23+
tags (None/list of str): 策略标签, 默认为调用 TqTradingUnit 的文件名及 account_id
24+
25+
Example1::
26+
27+
from tqsdk import TqApi, TqTradingUnit, TqAuth
28+
account = TqTradingUnit(account_id="众期子账户", tags=["铜品种策略", "套利策略"])
29+
api = TqApi(account, auth=TqAuth("快期账户", "账户密码"))
30+
31+
"""
32+
self._tags = self._convert_tags(tags, account_id)
33+
super(TqTradingUnit, self).__init__(broker_id="", account_id=account_id, password="tqsdk_zq", td_url="zq://localhost/")
34+
35+
def _convert_tags(self, tags: Optional[List[str]], account_id: str) -> list:
36+
if tags is None:
37+
frame = inspect.stack()[1]
38+
filename = frame.filename
39+
strategy_set = {filename, account_id}
40+
else:
41+
strategy_set = set(tags)
42+
return list(strategy_set)
43+
44+
@property
45+
def _account_auth(self):
46+
return {
47+
"feature": "tq_trading_unit",
48+
"account_id": None,
49+
"auto_add": False,
50+
}
51+
52+
def _get_account_key(self):
53+
s = self._account_id + self._td_url
54+
return hashlib.md5(s.encode('utf-8')).hexdigest()
55+
56+
async def _send_login_pack(self):
57+
req = {
58+
"aid": "req_login",
59+
"bid": "tqsdk_zq",
60+
"user_name": self._account_id,
61+
"password": "tqsdk_zq",
62+
"tags": self._tags
63+
}
64+
await self._td_send_chan.send(req)

0 commit comments

Comments
 (0)