Skip to content

Commit ded2cec

Browse files
shinny-packshinny-mayanqiong
authored andcommitted
Update Version 3.5.8
1 parent b263bae commit ded2cec

11 files changed

+154
-48
lines changed

PKG-INFO

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Metadata-Version: 2.1
22
Name: tqsdk
3-
Version: 3.5.7
3+
Version: 3.5.8
44
Summary: TianQin SDK
55
Home-page: https://www.shinnytech.com/tqsdk
66
Author: TianQin

doc/conf.py

+2-2
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.5.7'
51+
version = u'3.5.8'
5252
# The full version, including alpha/beta/rc tags.
53-
release = u'3.5.7'
53+
release = u'3.5.8'
5454

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

doc/version.rst

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
版本变更
44
=============================
5+
3.5.8 (2024/04/29)
6+
7+
* 增加::py:class:`~tqsdk.tools.DataDownloader` 增加 write_mode 参数,作为写入模式参数,并且在模式为 'a' 时,不写入标题行
8+
* 修复:用户在使用 :py:class:`~tqsdk.TargetPosScheduler` 时可能出现重复创建 :py:class:`~tqsdk.TargetPosTask` 实例的问题
9+
* 优化::py:class:`~tqsdk.tools.DataDownloader` 在下载没有任何成交数据的合约时能够及时退出,避免等待超时或报错
10+
* 优化:重构 :py:class:`~tqsdk.TargetPosTask` 退出时释放资源部分,优化异步代码中 :py:class:`~tqsdk.TargetPosTask` 的使用方法
11+
* 优化:回测时,如果订阅没有任何成交数据的合约,构造的空数据给下游,避免程序一直等待
12+
* 优化:对发送给合约服务器的数据进行检查,避免发送不合法的数据,提前报错通知用户
13+
14+
515
3.5.7 (2024/04/23)
616

717
* 修复::py:meth:`~tqsdk.TqApi.get_kline_data_series`、:py:meth:`~tqsdk.TqApi.get_tick_data_series` 接口在指定时间段没有数据时报错

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

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

tqsdk/__version__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '3.5.7'
1+
__version__ = '3.5.8'

tqsdk/api.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -2823,7 +2823,12 @@ async def show_symbols_info(symbols):
28232823
"""
28242824
if self._stock is False:
28252825
raise Exception("期货行情系统(_stock = False)不支持当前接口调用")
2826-
symbol_list = [symbol] if isinstance(symbol, str) else symbol
2826+
if isinstance(symbol, str):
2827+
symbol_list = [symbol]
2828+
else:
2829+
if len(symbol) == 0:
2830+
raise Exception("symbol 参数不能为空列表。")
2831+
symbol_list = symbol
28272832
if any([s == "" for s in symbol_list]):
28282833
raise Exception(f"symbol 参数 {symbol} 中不能有空字符串。")
28292834
backtest_timestamp = _datetime_to_timestamp_nano(self._get_current_datetime()) if isinstance(self._backtest,

tqsdk/backtest/backtest.py

+25-10
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ async def _generator_diffs(self, keep_current):
350350
# klines 请求,需要记录已经发送 api 的数据
351351
for symbol in diff.get("klines", {}):
352352
for dur in diff["klines"][symbol]:
353-
for kid in diff["klines"][symbol][dur]["data"]:
353+
for kid in diff["klines"][symbol][dur].get('data', {}):
354354
rs = self._sended_to_api.setdefault((symbol, int(dur)), [])
355355
kid = int(kid)
356356
self._sended_to_api[(symbol, int(dur))] = _rangeset_range_union(rs, (kid, kid + 1))
@@ -390,22 +390,24 @@ def _generator_quotes_diffs(self, quotes_helper) -> dict:
390390
# 如果先订阅 A 合约(有夜盘),时间停留在夜盘开始时间, 再订阅 B 合约(没有夜盘),那么 B 合约的行情(前一天收盘时间)应该发下去,
391391
# 否则 get_quote(B) 等到收到行情才返回,会直接把时间推进到第二天白盘。
392392
continue
393+
item = quotes_helper[key]["kline_or_tick"]
394+
if item is None:
395+
# item 如果是 None 的话,没有可以生成行情的信息的数据,那么不生成 quote_diff
396+
continue
393397
diffs = None
394398
if self._quotes[symbol]['min_duration'] == 0 and dur == 0:
395399
# tick 生成行情
396-
tick = quotes_helper[key]["kline_or_tick"]
397-
diffs = TqBacktest._get_quote_diffs_from_tick(symbol, tick)
400+
diffs = TqBacktest._get_quote_diffs_from_tick(symbol, item)
398401
if self._quotes[symbol]['min_duration'] != 0:
399402
# kline 生成行情
400403
when = quotes_helper[key]["when"]
401404
timestamp = quotes_helper[key]["timestamp"] # quote 行情时间
402-
kline = quotes_helper[key]["kline_or_tick"]
403405
quote_info = self._data["quotes"][symbol]
404406
froms = ["open"] if when == "OPEN" else ["close"]
405407
if when == "CLOSE" and self._quotes[symbol]['min_duration'] == dur:
406408
# kline 生成 quote 数据,只有该合约订阅的最小周期会生成 high low 对应的行情
407409
froms = ["high", "low", "close"]
408-
diffs = TqBacktest._get_quote_diffs_from_kline(symbol, quote_info['price_tick'], timestamp, kline, froms)
410+
diffs = TqBacktest._get_quote_diffs_from_kline(symbol, quote_info['price_tick'], timestamp, item, froms)
409411
if diffs:
410412
self._quotes[symbol]["sended_init_quote"] = True
411413
self._diffs.extend(diffs)
@@ -505,18 +507,31 @@ async def _gen_serial(self, ins, dur):
505507
if not (chart_info.items() <= _get_obj(chart, ["state"]).items()):
506508
# 当前请求还没收齐回应, 不应继续处理
507509
continue
508-
left_id = chart.get("left_id", -1)
509-
right_id = chart.get("right_id", -1)
510-
if (left_id == -1 and right_id == -1) or chart.get("more_data", True):
511-
continue # 定位信息还没收到, 数据没有完全收到
510+
if not chart.get("ready", False):
511+
continue # chart 数据还没准备好
512512
last_id = serials[0].get("last_id", -1)
513513
if last_id == -1:
514-
continue # 数据序列还没收到
514+
# 所有合约的 tick 数据一定有,开盘一定会收到一笔 tick
515+
# kline 是由有价格的 tick 生成的,所以 kline 可能没有数据的
516+
assert dur > 0
517+
diff = {
518+
"klines": {
519+
symbol_list[0]: {
520+
str(dur): {
521+
"last_id": -1
522+
}
523+
}
524+
}
525+
}
526+
yield self._current_dt, diff, None, "OPEN"
527+
return
515528
if self._data.get("mdhis_more_data", True):
516529
self._data["_listener"].add(update_chan)
517530
continue
518531
else:
519532
self._data["_listener"].discard(update_chan)
533+
left_id = chart.get("left_id", -1)
534+
right_id = chart.get("right_id", -1)
520535
if current_id is None:
521536
current_id = max(left_id, 0)
522537
# 发送下一段 chart 8964 根 kline

tqsdk/ins_schema.py

+16
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@
66
import sgqlc.types
77
from sgqlc.operation import Fragment
88

9+
10+
########################################################################
11+
# Monkey patching 检查请求的参数是否合法,与 api.query_graphql 函数的校验规则保持一致
12+
########################################################################
13+
_origin__to_graphql_input__ = sgqlc.types.Arg.__to_graphql_input__
14+
15+
16+
def _tqsdk__to_graphql_input__(self, value, *args, **kwargs):
17+
if value == "" or isinstance(value, list) and (any([s == "" for s in value]) or len(value) == 0):
18+
raise Exception(f"variables 中变量值不支持空字符串、空列表或者列表中包括空字符串。")
19+
return _origin__to_graphql_input__(self, value, *args, **kwargs)
20+
21+
22+
sgqlc.types.Arg.__to_graphql_input__ = _tqsdk__to_graphql_input__
23+
24+
925
ins_schema = sgqlc.types.Schema()
1026

1127

tqsdk/lib/target_pos_scheduler.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ async def _run(self):
135135
async for _ in self._api.register_update_notify(quote):
136136
if _get_trade_timestamp(quote.datetime, float('nan')) > row['deadline']:
137137
if target_pos_task:
138-
target_pos_task._task.cancel()
139-
await asyncio.gather(target_pos_task._task, return_exceptions=True)
138+
target_pos_task.cancel()
139+
await asyncio.gather(target_pos_task, return_exceptions=True)
140140
break
141141
elif target_pos_task: # 最后一项,如果有 target_pos_task 等待持仓调整完成,否则直接退出
142142
position = self._account.get_position(self._symbol)
@@ -147,8 +147,8 @@ async def _run(self):
147147
_index = _index + 1
148148
finally:
149149
if target_pos_task:
150-
target_pos_task._task.cancel()
151-
await asyncio.gather(target_pos_task._task, return_exceptions=True)
150+
target_pos_task.cancel()
151+
await asyncio.gather(target_pos_task, return_exceptions=True)
152152
await self._trade_objs_chan.close()
153153
await self._trade_recv_task
154154

tqsdk/lib/target_pos_task.py

+72-7
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,33 @@ def get_price(direction):
214214
self._time_update_task = self._api.create_task(self._update_time_from_md()) # 监听行情更新并记录当时本地时间的task
215215
self._local_time_record = time.time() - 0.005 # 更新最新行情时间时的本地时间
216216
self._local_time_record_update_chan = TqChan(self._api, last_only=True) # 监听 self._local_time_record 更新
217+
self._wait_task_finished = self._api._loop.create_future()
218+
self._task.add_done_callback(lambda _: self._api.create_task(self._exit_task()))
219+
220+
async def _exit_task(self):
221+
"""
222+
执行 task.cancel() 时, 删除掉该 symbol 对应的 TargetPosTask 实例,以释放占有的资源。
223+
224+
当用户代码为:
225+
t = TargetPosTask(api, 'SHFE.rb2106', min_volume=2, max_volume=10)
226+
t.cancel()
227+
await asyncio.gather(t._task, return_exceptions=True)
228+
229+
以上代码执行后,t._task 中的 finally 部分没有被执行过,因为 t._task 本身从来没有被执行过。
230+
231+
所以这里用 add_done_callback 的方式,处理 __init__ 方法中创建的资源。
232+
self._task、self._pos_chan、self._time_update_task 都是在 __init__ 方法里创建的资源,所以在这里释放资源,
233+
self._task 中的 finally 部分只处理在 self._task 函数里创建的资源。
234+
"""
235+
# self._account 类型为 TqSim/TqKq/TqAccount,都包括 _account_key 变量
236+
TargetPosTaskSingleton._instances.pop(self._account._account_key + "#" + self._symbol, None)
237+
await self._pos_chan.close()
238+
self._time_update_task.cancel()
239+
await asyncio.gather(self._time_update_task, return_exceptions=True)
240+
self._wait_task_finished.set_result(True)
241+
242+
def __await__(self):
243+
return self._wait_task_finished.__await__()
217244

218245
def set_target_volume(self, volume: int) -> None:
219246
"""
@@ -379,12 +406,7 @@ async def _target_pos_task(self):
379406
all_tasks.append(order_task)
380407
delta_volume -= order_volume if order_dir == "BUY" else -order_volume
381408
finally:
382-
# 执行 task.cancel() 时, 删除掉该 symbol 对应的 TargetPosTask 实例
383-
# self._account 类型为 TqSim/TqKq/TqAccount,都包括 _account_key 变量
384-
TargetPosTaskSingleton._instances.pop(self._account._account_key + "#" + self._symbol, None)
385-
await self._pos_chan.close()
386-
self._time_update_task.cancel()
387-
await asyncio.gather(*([t._task for t in all_tasks] + [self._time_update_task]), return_exceptions=True)
409+
await asyncio.gather(*[t._task for t in all_tasks], return_exceptions=True)
388410

389411
def cancel(self):
390412
"""
@@ -423,6 +445,46 @@ def cancel(self):
423445
424446
api.close()
425447
448+
449+
Example2::
450+
451+
# 在异步代码中使用
452+
from datetime import datetime, time
453+
from tqsdk import TqApi, TargetPosTask
454+
455+
api = TqApi(auth=TqAuth("快期账户", "账户密码"))
456+
quote = api.get_quote("SHFE.rb2110")
457+
458+
async def demo(SYMBOL):
459+
quote = await api.get_quote(SYMBOL)
460+
target_pos_passive = TargetPosTask(api, SYMBOL, price="PASSIVE")
461+
async with api.register_update_notify() as update_chan:
462+
async for _ in update_chan:
463+
if datetime.strptime(quote.datetime, "%Y-%m-%d %H:%M:%S.%f").time() < time(14, 50):
464+
# ... 策略代码 ...
465+
else:
466+
target_pos_passive.cancel() # 取消 TargetPosTask 实例
467+
await target_pos_passive # 等待 target_pos_passive 处理 cancel 结束
468+
break
469+
470+
target_pos_active = TargetPosTask(api, "SHFE.rb2110", price="ACTIVE")
471+
target_pos_active.set_target_volume(0) # 平所有仓位
472+
pos = await api.get_position(SYMBOL)
473+
async with api.register_update_notify() as update_chan:
474+
async for _ in update_chan:
475+
if pos.pos == 0:
476+
target_pos_active.cancel() # 取消 TargetPosTask 实例
477+
await target_pos_active # 等待 target_pos_active 处理 cancel 结束
478+
break
479+
480+
481+
symbol_list = ["SHFE.rb2107", "DCE.m2109"] # 设置合约代码
482+
for symbol in symbol_list:
483+
api.create_task(demo("SHFE.rb2107")) # 为每个合约创建异步任务
484+
485+
while True:
486+
api.wait_update()
487+
426488
"""
427489
self._task.cancel()
428490

@@ -433,7 +495,10 @@ def is_finished(self) -> bool:
433495
Returns:
434496
bool: 当前 TargetPosTask 实例是否已经结束
435497
"""
436-
return self._task.done()
498+
if self._wait_task_finished.done():
499+
assert self._task.done() is True
500+
return self._wait_task_finished.result()
501+
return False
437502

438503

439504
class InsertOrderUntilAllTradedTask(object):

0 commit comments

Comments
 (0)