diff --git a/PKG-INFO b/PKG-INFO
index 63efdd18..f24ecd5f 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: tqsdk
-Version: 3.4.10
+Version: 3.4.11
Summary: TianQin SDK
Home-page: https://www.shinnytech.com/tqsdk
Author: TianQin
diff --git a/doc/conf.py b/doc/conf.py
index 5f6a7868..430fc298 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -40,7 +40,7 @@
# General information about the project.
project = u'TianQin Python SDK'
-copyright = u'2018-2023, TianQin'
+copyright = u'2018-2024, TianQin'
author = u'TianQin'
# The version info for the project you're documenting, acts as replacement for
@@ -48,9 +48,9 @@
# built documents.
#
# The short X.Y version.
-version = u'3.4.10'
+version = u'3.4.11'
# The full version, including alpha/beta/rc tags.
-release = u'3.4.10'
+release = u'3.4.11'
# 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 91c3a602..5a19c058 100644
--- a/doc/profession.rst
+++ b/doc/profession.rst
@@ -20,9 +20,9 @@ TqSdk 中大部分功能是供用户免费使用的, 同时我们也提供了专
.. figure:: images/how-grafana04.gif
- `快期专业版官网地址 `_
+ `快期专业版官网地址 `_
- `快期专业版文档地址 `_
+ `快期专业版文档地址 `_
更稳定的行情服务器
-------------------------------------------------
diff --git a/doc/usage/backtest.rst b/doc/usage/backtest.rst
index 04bab1bb..7466f703 100644
--- a/doc/usage/backtest.rst
+++ b/doc/usage/backtest.rst
@@ -202,7 +202,7 @@ TqSdk回测框架使用一套复杂的规则来推进行情:
规则3: quote按照以下规则更新::
if 策略程序中使用了这个合约的tick序列:
- 每次tick序列推进时会更新quote的这些字段 datetime/ask&bid_price1/ask&bid_volume1/last_price/highest/lowest/average/volume/amount/open_interest/price_tick/price_decs/volume_multiple/max&min_limit&market_order_volume/underlying_symbol/strike_price
+ 每次tick序列推进时会更新quote的这些字段 datetime/ask&bid_price1至ask&bid_price5/ask&bid_volume1至ask&bid_volume5/last_price/highest/lowest/average/volume/amount/open_interest/price_tick/price_decs/volume_multiple/max&min_limit&market_order_volume/underlying_symbol/strike_price
elif 策略程序中使用了这个合约的K线序列:
每次K线序列推进时会更新quote. 使用 k线生成的 quote 的盘口由收盘价分别加/减一个最小变动单位, 并且 highest/lowest/average/amount 始终为 nan, volume 始终为0.
每次K线序列推进时会更新quote的这些字段 datetime/ask&bid_price1/ask&bid_volume1/last_price/open_interest/price_tick/price_decs/volume_multiple/max&min_limit&market_order_volume/underlying_symbol/strike_price
diff --git a/doc/version.rst b/doc/version.rst
index 48a8d36e..bcc991d7 100644
--- a/doc/version.rst
+++ b/doc/version.rst
@@ -2,6 +2,13 @@
版本变更
=============================
+3.4.11 (2024/01/03)
+
+* 优化:支持天勤在不同时区设置的操作系统上使用。tqsdk 内部时间表示全部使用北京时间。
+ 对于以下接口,用户输入 datetime 类型参数时,如果未指定时区信息,tqsdk 会指定为北京时间;如果指定了时区信息,会转为北京时间,保证 Unix Timestamp 时间戳不变。
+ :py:meth:`~tqsdk.TqApi.get_kline_data_series`、:py:meth:`~tqsdk.TqApi.get_tick_data_series`、:py:class:`~tqsdk.tools.DataDownloader`、:py:class:`~tqsdk.TqBacktest`。
+
+
3.4.10 (2023/09/22)
* 修复:pandas 2.1.0 版本 fillna 、NumericBlock 会报 deprecated warning 的问题
diff --git a/setup.py b/setup.py
index 35be3a7d..8e064c51 100644
--- a/setup.py
+++ b/setup.py
@@ -8,7 +8,7 @@
setuptools.setup(
name='tqsdk',
- version="3.4.10",
+ version="3.4.11",
description='TianQin SDK',
author='TianQin',
author_email='tianqincn@gmail.com',
diff --git a/tqsdk/__version__.py b/tqsdk/__version__.py
index b51de07f..90fb0789 100644
--- a/tqsdk/__version__.py
+++ b/tqsdk/__version__.py
@@ -1 +1 @@
-__version__ = '3.4.10'
+__version__ = '3.4.11'
diff --git a/tqsdk/algorithm/time_table_generater.py b/tqsdk/algorithm/time_table_generater.py
index fbe295be..0c5feff0 100644
--- a/tqsdk/algorithm/time_table_generater.py
+++ b/tqsdk/algorithm/time_table_generater.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
__author__ = 'mayanqiong'
-from datetime import datetime, time, timedelta
+from datetime import time, timedelta
from typing import Optional, Union
import numpy as np
@@ -12,7 +12,7 @@
from tqsdk.api import TqApi
from tqsdk import utils
from tqsdk.datetime import _get_trading_timestamp, _get_trade_timestamp, _get_trading_day_from_timestamp, \
- _datetime_to_timestamp_nano
+ _datetime_to_timestamp_nano, _timestamp_nano_to_datetime
from tqsdk.rangeset import _rangeset_slice, _rangeset_head
from tqsdk.tradeable import TqAccount, TqKq, TqSim
@@ -214,8 +214,8 @@ def vwap_table(api: TqApi, symbol: str, target_pos: int, duration: float,
# 获取 Kline
klines = api.get_kline_serial(symbol, TIME_CELL, data_length=int(10 * 60 * 60 / TIME_CELL * HISTORY_DAY_LENGTH))
- klines["time"] = klines.datetime.apply(lambda x: datetime.fromtimestamp(x // 1000000000).time()) # k线时间
- klines["date"] = klines.datetime.apply(lambda x: datetime.fromtimestamp(_get_trading_day_from_timestamp(x) // 1000000000).date()) # k线交易日
+ klines["time"] = klines.datetime.apply(lambda x: _timestamp_nano_to_datetime(x).time()) # k线时间
+ klines["date"] = klines.datetime.apply(lambda x: _timestamp_nano_to_datetime(_get_trading_day_from_timestamp(x)).date()) # k线交易日
quote = api.get_quote(symbol)
# 当前交易日完整的交易时间段
@@ -226,7 +226,7 @@ def vwap_table(api: TqApi, symbol: str, target_pos: int, duration: float,
if not trading_timestamp_nano_range[0][0] <= current_timestamp_nano < trading_timestamp_nano_range[-1][1]:
raise Exception("当前时间不在指定的交易时间段内")
- current_datetime = datetime.fromtimestamp(current_timestamp_nano//1000000000)
+ current_datetime = _timestamp_nano_to_datetime(current_timestamp_nano)
# 下一分钟的开始时间
next_datetime = current_datetime.replace(second=0) + timedelta(minutes=1)
start_datetime_nano = _datetime_to_timestamp_nano(next_datetime)
@@ -234,8 +234,8 @@ def vwap_table(api: TqApi, symbol: str, target_pos: int, duration: float,
if not (r and trading_timestamp_nano_range[0][0] <= r[-1][-1] < trading_timestamp_nano_range[-1][1]):
raise Exception("指定时间段超出当前交易日")
- start_datetime = datetime.fromtimestamp(start_datetime_nano // 1000000000)
- end_datetime = datetime.fromtimestamp((r[-1][-1] - 1) // 1000000000)
+ start_datetime = _timestamp_nano_to_datetime(start_datetime_nano)
+ end_datetime = _timestamp_nano_to_datetime((r[-1][-1] - 1))
time_slot_start = time(start_datetime.hour, start_datetime.minute) # 计划交易时段起始时间点
time_slot_end = time(end_datetime.hour, end_datetime.minute) # 计划交易时段终点时间点
if time_slot_end > time_slot_start: # 判断是否类似 23:00:00 开始, 01:00:00 结束这样跨天的情况
diff --git a/tqsdk/api.py b/tqsdk/api.py
index 5418dfec..21e71b67 100644
--- a/tqsdk/api.py
+++ b/tqsdk/api.py
@@ -56,8 +56,8 @@
from tqsdk.calendar import _get_trading_calendar, TqContCalendar, _init_chinese_rest_days
from tqsdk.data_extension import DataExtension
from tqsdk.data_series import DataSeries
-from tqsdk.datetime import _get_trading_day_start_time, _get_trading_day_end_time, _get_trading_day_from_timestamp, \
- _datetime_to_timestamp_nano
+from tqsdk.datetime import _get_trading_day_from_timestamp, _datetime_to_timestamp_nano, _timestamp_nano_to_datetime, \
+ _cst_now, _convert_user_input_to_nano
from tqsdk.diff import _merge_diff, _get_obj, _is_key_exist, _register_update_chan
from tqsdk.entity import Entity
from tqsdk.exceptions import TqTimeoutError
@@ -305,7 +305,7 @@ def __init__(self, account: Optional[Union[TqMultiAccount, UnionTradeable]] = No
def _print(self, msg: str = "", level: str = "INFO"):
if self.disable_print:
return
- dt = "" if self._backtest else datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ dt = "" if self._backtest else _cst_now().strftime('%Y-%m-%d %H:%M:%S')
level = level if isinstance(level, str) else logging.getLevelName(level)
print(f"{(dt + ' - ') if dt else ''}{level:>8} - {msg}")
@@ -836,9 +836,15 @@ def get_kline_data_series(self, symbol: Union[str, List[str]], duration_seconds:
duration_seconds (int): K 线数据周期, 以秒为单位。例如: 1 分钟线为 60,1 小时线为 3600,日线为 86400。\
注意: 周期在日线以内时此参数可以任意填写, 在日线以上时只能是日线(86400)的整数倍
- start_dt (date/datetime): 起始时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点
+ start_dt (date/datetime): 起始时间
+ * date: 指的是交易日
- end_dt (date/datetime): 结束时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点
+ * datetime: 指的是具体时间点,如果没有指定时区信息,则默认为北京时间
+
+ end_dt (date/datetime): 结束时间
+ * date: 指的是交易日
+
+ * datetime: 指的是具体时间点,如果没有指定时区信息,则默认为北京时间
adj_type (str/None): [可选]指定复权类型,默认为 None。adj_type 参数只对股票和基金类型合约有效。\
"F" 表示前复权;"B" 表示后复权;None 表示不做处理。
@@ -896,9 +902,15 @@ def get_tick_data_series(self, symbol: Union[str, List[str]], start_dt: Union[da
Args:
symbol (str): 指定合约代码。当前只支持单个合约。
- start_dt (date/datetime): 起始时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点
+ start_dt (date/datetime): 起始时间
+ * date: 指的是交易日
+
+ * datetime: 指的是具体时间点,如果没有指定时区信息,则默认为北京时间
- end_dt (date/datetime): 结束时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点
+ end_dt (date/datetime): 结束时间
+ * date: 指的是交易日
+
+ * datetime: 指的是具体时间点,如果没有指定时区信息,则默认为北京时间
adj_type (str/None): [可选]指定复权类型,默认为 None。adj_type 参数只对股票和基金类型合约有效。\
"F" 表示前复权;"B" 表示后复权;None 表示不做处理。
@@ -954,20 +966,7 @@ def _get_data_series(self, call_func: str, symbol_list: Union[str, List[str]], d
if len(symbol_list) != 1:
raise Exception(f"{call_func} 数据获取方式暂不支持多合约请求")
self._ensure_symbol(symbol_list) # 检查合约代码是否存在
- if isinstance(start_dt, datetime):
- start_dt_nano = _datetime_to_timestamp_nano(start_dt)
- elif isinstance(start_dt, date):
- start_dt_nano = _get_trading_day_start_time(
- _datetime_to_timestamp_nano(datetime(start_dt.year, start_dt.month, start_dt.day)))
- else:
- raise Exception(f"start_dt 参数类型 {type(start_dt)} 错误, 只支持 datetime / date 类型,请检查是否正确")
- if isinstance(end_dt, datetime):
- end_dt_nano = _datetime_to_timestamp_nano(end_dt)
- elif isinstance(end_dt, date):
- end_dt_nano = _get_trading_day_end_time(
- _datetime_to_timestamp_nano(datetime(end_dt.year, end_dt.month, end_dt.day)))
- else:
- raise Exception(f"end_dt 参数类型 {type(end_dt)} 错误, 只支持 datetime / date 类型,请检查是否正确")
+ start_dt_nano, end_dt_nano = _convert_user_input_to_nano(start_dt, end_dt)
if adj_type not in [None, "F", "B"]:
raise Exception("adj_type 参数只支持 None (不复权) | 'F' (前复权) | 'B' (后复权) ")
ds = DataSeries(self, symbol_list, dur_nano, start_dt_nano, end_dt_nano, adj_type)
@@ -981,17 +980,23 @@ def _get_data_series(self, call_func: str, symbol_list: Union[str, List[str]], d
# ----------------------------------------------------------------------
def get_trading_calendar(self, start_dt: Union[date, datetime], end_dt: Union[date, datetime]) -> pd.DataFrame:
"""
- 获取一段时间内的交易日历信息,交易日历可以处理的范围为 2003-01-01 ~ 2022-12-31。
+ 获取一段时间内的交易日历信息,交易日历可以处理的范围为 2003-01-01 ~ 2024-12-31。
Args:
- start_dt (date/datetime): 起始时间,如果类型为 date 则指的是该日期;如果为 datetime 则指的是该时间点所在日期
+ start_dt (date/datetime): 起始时间
+ * date: 指的是交易日
+
+ * datetime: 指的指的是该时间点所在年月日日期
+
+ end_dt (date/datetime): 结束时间
+ * date: 指的是交易日
- end_dt (date/datetime): 结束时间,如果类型为 date 则指的是该日期;如果为 datetime 则指的是该时间点所在日期
+ * datetime: 指的指的是该时间点所在年月日日期
Returns:
pandas.DataFrame: 包含以下列:
- * date: (datetime64[ns]) 日期
+ * date: (datetime64[ns]) 日期,为北京时间的日期
* trading: (bool) 是否是交易日
Example::
@@ -1016,11 +1021,11 @@ def get_trading_calendar(self, start_dt: Union[date, datetime], end_dt: Union[da
api.close()
"""
if isinstance(start_dt, datetime):
- start_dt = date(year=start_dt.year, month=start_dt.month, day=start_dt.day)
+ start_dt = start_dt.date()
elif not isinstance(start_dt, date):
raise Exception(f"start_dt 参数类型 {type(start_dt)} 错误, 只支持 datetime / date 类型,请检查是否正确")
if isinstance(end_dt, datetime):
- end_dt = date(year=end_dt.year, month=end_dt.month, day=end_dt.day)
+ end_dt = end_dt.date()
elif not isinstance(end_dt, date):
raise Exception(f"end_dt 参数类型 {type(end_dt)} 错误, 只支持 datetime / date 类型,请检查是否正确")
first_date, latest_date = _init_chinese_rest_days()
@@ -1078,9 +1083,9 @@ def query_his_cont_quotes(self, symbol: Union[str, List[str]], n: int = 200) ->
raise Exception(f"参数错误,n={n} 应该是大于等于 1 的整数")
now_dt = self._get_current_datetime()
trading_day = _get_trading_day_from_timestamp(_datetime_to_timestamp_nano(now_dt))
- end_dt = datetime.fromtimestamp(trading_day / 1000000000)
- cont_calendar = TqContCalendar(start_dt=end_dt - timedelta(days=n * 2 + 30), end_dt=end_dt, symbols=symbols,
- headers=self._base_headers)
+ end_dt = _timestamp_nano_to_datetime(trading_day)
+ cont_calendar = TqContCalendar(start_dt=(end_dt - timedelta(days=n * 2 + 30)).date(), end_dt=end_dt.date(),
+ symbols=symbols, headers=self._base_headers)
df = cont_calendar.df.loc[cont_calendar.df.date.le(end_dt), ['date'] + symbols]
df = df.iloc[-n:]
df.reset_index(inplace=True, drop=True)
@@ -2575,10 +2580,8 @@ def filter(query_result):
for edge in quote["derivatives"]["edges"]:
option = edge["node"]
if (option_class and option["call_or_put"] != option_class) \
- or (exe_year and datetime.fromtimestamp(
- option["last_exercise_datetime"] / 1e9).year != exe_year) \
- or (exe_month and datetime.fromtimestamp(
- option["last_exercise_datetime"] / 1e9).month != exe_month) \
+ or (exe_year and _timestamp_nano_to_datetime(option["last_exercise_datetime"]).year != exe_year) \
+ or (exe_month and _timestamp_nano_to_datetime(option["last_exercise_datetime"]).month != exe_month) \
or (strike_price and option["strike_price"] != strike_price) \
or (expired is not None and option["expired"] != expired) \
or (has_A is True and option["english_name"].count('A') == 0) \
@@ -2740,8 +2743,8 @@ def query_symbol_info(self, symbol: Union[str, List[str]]) -> TqSymbolDataFrame:
* pre_settlement: 昨结算
* pre_open_interest: 昨持仓
* pre_close: 昨收盘
- * trading_time_day: 白盘交易时间段,list 类型
- * trading_time_night: 夜盘交易时间段,list 类型
+ * trading_time_day: 白盘交易时间段,pandas.Series 类型
+ * trading_time_night: 夜盘交易时间段,pandas.Series 类型
注意:
@@ -2999,7 +3002,7 @@ def _convert_query_result_to_list(self, query_result):
for quote in query_result.get("result", {}).get("multi_symbol_info", []):
if quote.get("derivatives"):
for edge in quote["derivatives"]["edges"]:
- last_exercise_datetime = datetime.fromtimestamp(edge["node"]["last_exercise_datetime"] / 1e9)
+ last_exercise_datetime = _timestamp_nano_to_datetime(edge["node"]["last_exercise_datetime"])
edge["node"]["exercise_year"] = last_exercise_datetime.year
edge["node"]["exercise_month"] = last_exercise_datetime.month
options.append(edge["node"])
@@ -3157,7 +3160,7 @@ def _setup_connection(self):
try:
self._md_url = self._auth._get_md_url(self._stock, backtest=isinstance(self._backtest, TqBacktest)) # 如果用户未指定行情地址,则使用名称服务获取行情地址
except Exception as e:
- now = datetime.now()
+ now = _cst_now()
if now.hour == 19 and 0 <= now.minute <= 30:
raise Exception(f"{e}, 每日 19:00-19:30 为日常运维时间,请稍后再试")
else:
@@ -3178,8 +3181,8 @@ def _setup_connection(self):
# 期权增加了 exercise_year、exercise_month 在旧版合约服务中没有,需要添加,使用下市日期代替最后行权日
for quote in quotes.values():
if quote["ins_class"] == "FUTURE_OPTION":
- quote["exercise_year"] = datetime.fromtimestamp(quote["expire_datetime"]).year
- quote["exercise_month"] = datetime.fromtimestamp(quote["expire_datetime"]).month
+ quote["exercise_year"] = _timestamp_nano_to_datetime(int(quote["expire_datetime"] * 1000000) * 1000).year
+ quote["exercise_month"] = _timestamp_nano_to_datetime(int(quote["expire_datetime"] * 1000000) * 1000).month
ws_md_recv_chan.send_nowait({
"aid": "rtn_data",
"data": [{"quotes": quotes}]
@@ -3952,9 +3955,9 @@ def _symbols_to_quotes(self, symbols, keys=None):
def _get_current_datetime(self):
if isinstance(self._backtest, TqBacktest):
current_dt = self._data.get('_tqsdk_backtest', {}).get('current_dt', 0)
- return datetime.fromtimestamp(current_dt / 1e9)
+ return _timestamp_nano_to_datetime(current_dt)
else:
- return datetime.now()
+ return _cst_now()
print("在使用天勤量化之前,默认您已经知晓并同意以下免责条款,如果不同意请立即停止使用:https://www.shinnytech.com/blog/disclaimer/", file=sys.stderr)
diff --git a/tqsdk/backtest/backtest.py b/tqsdk/backtest/backtest.py
index aed8092d..d9d0b585 100644
--- a/tqsdk/backtest/backtest.py
+++ b/tqsdk/backtest/backtest.py
@@ -12,7 +12,7 @@
from tqsdk.backtest.utils import TqBacktestContinuous, TqBacktestDividend
from tqsdk.channel import TqChan
from tqsdk.datetime import _get_trading_day_start_time, _get_trading_day_end_time, _get_trading_day_from_timestamp, \
- _timestamp_nano_to_str, _datetime_to_timestamp_nano
+ _timestamp_nano_to_str, _convert_user_input_to_nano
from tqsdk.diff import _merge_diff, _get_obj
from tqsdk.entity import Entity
from tqsdk.exceptions import BacktestFinished
@@ -64,27 +64,20 @@ class TqBacktest(object):
def __init__(self, start_dt: Union[date, datetime], end_dt: Union[date, datetime]) -> None:
"""
- 创建天勤回测类
+ 创建天勤回测类,起始时间和结束时间都应该是北京时间
Args:
- start_dt (date/datetime): 回测起始时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点
+ start_dt (date/datetime): 回测起始时间
+ * date: 指的是交易日
- end_dt (date/datetime): 回测结束时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点
+ * datetime: 指的是具体时间点,如果没有指定时区信息,则默认为北京时间
+
+ end_dt (date/datetime): 回测结束时间
+ * date: 指的是交易日
+
+ * datetime: 指的是具体时间点,如果没有指定时区信息,则默认为北京时间
"""
- if isinstance(start_dt, datetime):
- self._start_dt = _datetime_to_timestamp_nano(start_dt)
- elif isinstance(start_dt, date):
- self._start_dt = _get_trading_day_start_time(
- _datetime_to_timestamp_nano(datetime(start_dt.year, start_dt.month, start_dt.day)))
- else:
- raise Exception("回测起始时间(start_dt)类型 %s 错误, 请检查 start_dt 数据类型是否填写正确" % (type(start_dt)))
- if isinstance(end_dt, datetime):
- self._end_dt = _datetime_to_timestamp_nano(end_dt)
- elif isinstance(end_dt, date):
- self._end_dt = _get_trading_day_end_time(
- _datetime_to_timestamp_nano(datetime(end_dt.year, end_dt.month, end_dt.day)))
- else:
- raise Exception("回测结束时间(end_dt)类型 %s 错误, 请检查 end_dt 数据类型是否填写正确" % (type(end_dt)))
+ self._start_dt, self._end_dt = _convert_user_input_to_nano(start_dt, end_dt)
self._current_dt = self._start_dt
# 记录当前的交易日 开始时间/结束时间
self._trading_day = _get_trading_day_from_timestamp(self._current_dt)
diff --git a/tqsdk/backtest/utils.py b/tqsdk/backtest/utils.py
index 64487aa3..ec003e2f 100644
--- a/tqsdk/backtest/utils.py
+++ b/tqsdk/backtest/utils.py
@@ -8,6 +8,7 @@
import requests
from tqsdk.calendar import TqContCalendar
+from tqsdk.datetime import _timestamp_nano_to_datetime, _timestamp_nano_to_str
class TqBacktestContinuous(object):
@@ -18,12 +19,12 @@ def __init__(self, start_dt: int, end_dt: int, headers=None) -> None:
start_dt 开始的交易日
end_dt 结束的交易日
"""
- self._cont_calendar = TqContCalendar(start_dt=datetime.fromtimestamp(start_dt / 1e9),
- end_dt=datetime.fromtimestamp(end_dt / 1e9),
+ self._cont_calendar = TqContCalendar(start_dt=_timestamp_nano_to_datetime(start_dt).date(),
+ end_dt=_timestamp_nano_to_datetime(end_dt).date(),
headers=headers)
- def _get_history_cont_quotes(self, trading_day):
- df = self._cont_calendar._get_cont_underlying_on_date(dt=datetime.fromtimestamp(trading_day / 1e9))
+ def _get_history_cont_quotes(self, trading_day: int):
+ df = self._cont_calendar._get_cont_underlying_on_date(trading_day=trading_day)
quotes = {k: {"underlying_symbol": df.iloc[0][k]} for k in df.columns if k.startswith("KQ.m")}
return quotes
@@ -39,12 +40,12 @@ def __init__(self, start_dt: int, end_dt: int, headers=None) -> None:
self._headers = headers
self._start_dt = start_dt
self._end_dt = end_dt
- self._start_date = datetime.fromtimestamp(self._start_dt / 1000000000).strftime('%Y%m%d')
- self._end_date = datetime.fromtimestamp(self._end_dt / 1000000000).strftime('%Y%m%d')
+ self._start_date = _timestamp_nano_to_str(self._start_dt, fmt='%Y%m%d')
+ self._end_date = _timestamp_nano_to_str(self._end_dt, fmt='%Y%m%d')
self._stocks = {} # 记录全部股票合约及从 stock-dividend 服务获取的原始数据
def _get_dividend(self, quotes, trading_day):
- dt = datetime.fromtimestamp(trading_day / 1000000000).strftime('%Y%m%d')
+ dt = _timestamp_nano_to_str(trading_day, fmt='%Y%m%d')
self._request_stock_dividend(quotes)
rsp_quotes = {}
# self._stocks 中应该已经记录了 quotes 中全部股票合约
diff --git a/tqsdk/calendar.py b/tqsdk/calendar.py
index 9b47507e..912a5e3d 100644
--- a/tqsdk/calendar.py
+++ b/tqsdk/calendar.py
@@ -6,12 +6,14 @@
__author__ = 'mayanqiong'
import os
-from datetime import date, datetime
+from datetime import date
from typing import Union, List
import pandas as pd
import requests
+from tqsdk.datetime import _cst_tz, _timestamp_nano_to_datetime
+
rest_days_df = None
chinese_holidays_range = None
@@ -78,6 +80,7 @@ def __init__(self, start_dt: date, end_dt: date, symbols: Union[List[str], None]
self.df = _get_trading_calendar(start_dt=start_dt, end_dt=end_dt, headers=headers)
self.df = self.df.loc[self.df.trading, ['date']] # 只保留交易日
self.df.reset_index(inplace=True, drop=True)
+ self.df['_cst_date'] = self.df.date.dt.tz_localize(_cst_tz) # 增加一列,将 date 转换为东八区时间, tqsdk 内部使用
if TqContCalendar.continuous is None:
rsp = requests.get(os.getenv("TQ_CONT_TABLE_URL", "https://files.shinnytech.com/continuous_table.json"), headers=headers) # 下载历史主连合约信息
rsp.raise_for_status()
@@ -86,7 +89,7 @@ def __init__(self, start_dt: date, end_dt: date, symbols: Union[List[str], None]
if not all([s in TqContCalendar.continuous.keys() for s in symbols]):
raise Exception(f"参数错误,symbols={symbols} 中应该全部都是主连合约代码")
symbols = TqContCalendar.continuous.keys() if symbols is None else symbols
- self.start_dt, self.end_dt = self.df.iloc[0].date, self.df.iloc[-1].date
+ self.start_dt, self.end_dt = self.df.iloc[0]._cst_date, self.df.iloc[-1]._cst_date
for s in symbols:
self._ensure_cont_on_df(s)
@@ -94,13 +97,17 @@ def _ensure_cont_on_df(self, cont):
"""将一个主连对应的标的填在 self.df 对应位置"""
temp_df = pd.DataFrame(data=TqContCalendar.continuous[cont], columns=['date', 'underlying'])
temp_df['date'] = pd.Series(pd.to_datetime(temp_df['date'], format='%Y%m%d'))
- merge_result = pd.merge(temp_df, self.df, sort=True, how="outer", on="date")
+ temp_df['_cst_date'] = temp_df.date.dt.tz_localize(_cst_tz) # 增加一列,将 date 转换为东八区时间
+ merge_result = pd.merge(temp_df, self.df, sort=True, how="outer", on="_cst_date")
merge_result.ffill(inplace=True)
merge_result.fillna(value="", inplace=True)
- s = merge_result.loc[merge_result.date.ge(self.start_dt) & merge_result.date.le(self.end_dt), 'underlying']
+ s = merge_result.loc[merge_result._cst_date.ge(self.start_dt) & merge_result._cst_date.le(self.end_dt), 'underlying']
self.df[cont] = pd.Series(s.values)
- def _get_cont_underlying_on_date(self, dt: datetime):
+ def _get_cont_underlying_on_date(self, trading_day: int):
"""返回某一交易日的全部主连"""
- df = self.df.loc[self.df.date.ge(dt), :]
+ # trading_day 为北京时间的某一天的 00:00:00 的时间戳
+ # 所以不能直接和 date 列比较,考虑到时区问题,增加 _cst_date 列,先转换为东八区时间,再比较
+ dt = _timestamp_nano_to_datetime(trading_day)
+ df = self.df.loc[self.df._cst_date.ge(dt), :]
return df.iloc[0:1]
diff --git a/tqsdk/connect.py b/tqsdk/connect.py
index f76780cc..f0d04c4c 100644
--- a/tqsdk/connect.py
+++ b/tqsdk/connect.py
@@ -10,7 +10,6 @@
import warnings
import base64
from abc import abstractmethod
-from datetime import datetime
from logging import Logger
from queue import Queue
from typing import Optional
@@ -20,6 +19,7 @@
import websockets
from shinny_structlog import ShinnyLoggerAdapter
+from tqsdk.datetime import _cst_now
from tqsdk.diff import _merge_diff, _get_obj
from tqsdk.entity import Entity
from tqsdk.exceptions import TqBacktestPermissionError
@@ -203,7 +203,7 @@ async def _run(self, api, url, send_chan, recv_chan):
except (websockets.exceptions.ConnectionClosed, websockets.exceptions.InvalidStatusCode, websockets.exceptions.InvalidURI,
websockets.exceptions.InvalidState, websockets.exceptions.ProtocolError, OSError, EOFError,
TqBacktestPermissionError) as e:
- in_ops_time = datetime.now().hour == 19 and 0 <= datetime.now().minute <= 30
+ in_ops_time = _cst_now().hour == 19 and 0 <= _cst_now().minute <= 30
# 发送网络连接断开的通知,code = 2019112911
notify_id = _generate_uuid()
notify = {
diff --git a/tqsdk/datetime.py b/tqsdk/datetime.py
index 2a4a5153..5234baef 100644
--- a/tqsdk/datetime.py
+++ b/tqsdk/datetime.py
@@ -8,15 +8,59 @@
import datetime
import time
+from typing import Union
+
+# 北京时间时区 CST (China Standard Time)
+_cst_tz = datetime.timezone(datetime.timedelta(hours=8))
+
+
+def _cst_now():
+ """返回当前北京时间 datetime.datetime 类型"""
+ return datetime.datetime.now(tz=_cst_tz)
+
+
+def _convert_to_cst_datetime(user_dt: Union[datetime.date, datetime.datetime]):
+ # 将用户输入的时间转换为东八区时间
+ if isinstance(user_dt, datetime.datetime):
+ if user_dt.tzinfo == _cst_tz:
+ return user_dt
+ return user_dt.replace(tzinfo=_cst_tz) if user_dt.tzinfo is None else user_dt.astimezone(tz=_cst_tz)
+ else:
+ return datetime.datetime(user_dt.year, user_dt.month, user_dt.day, tzinfo=_cst_tz)
+
+
+def _convert_user_input_to_nano(start_dt: Union[datetime.date, datetime.datetime],
+ end_dt: Union[datetime.date, datetime.datetime]) -> (int, int):
+ # 将用户输入的时间转换为对应交易时间段的纳秒时间戳
+ if isinstance(start_dt, datetime.datetime):
+ start_nano = _datetime_to_timestamp_nano(_convert_to_cst_datetime(start_dt))
+ elif isinstance(start_dt, datetime.date):
+ start_nano = _get_trading_day_start_time(_datetime_to_timestamp_nano(_convert_to_cst_datetime(start_dt)))
+ else:
+ raise Exception(f"start_dt 参数类型 {type(start_dt)} 错误, 只支持 datetime / date 类型,请检查是否正确")
+ if isinstance(end_dt, datetime.datetime):
+ end_nano = _datetime_to_timestamp_nano(_convert_to_cst_datetime(end_dt))
+ elif isinstance(end_dt, datetime.date):
+ end_nano = _get_trading_day_end_time(_datetime_to_timestamp_nano(_convert_to_cst_datetime(end_dt)))
+ else:
+ raise Exception(f"end_dt 参数类型 {type(end_dt)} 错误, 只支持 datetime / date 类型,请检查是否正确")
+ return start_nano, end_nano
def _datetime_to_timestamp_nano(dt: datetime.datetime) -> int:
+ # tqsdk 内部时间必须为东八区时间
+ if dt.tzinfo != _cst_tz:
+ dt = dt.replace(tzinfo=_cst_tz) if dt.tzinfo is None else dt.astimezone(tz=_cst_tz)
# timestamp() 返回值精度为 microsecond,直接乘以 1e9 可能有精度问题
return int(dt.timestamp() * 1000000) * 1000
+def _timestamp_nano_to_datetime(nano: int) -> datetime.datetime:
+ return datetime.datetime.fromtimestamp((nano // 1000) / 1000000, tz=_cst_tz)
+
+
def _timestamp_nano_to_str(nano: int, fmt="%Y-%m-%d %H:%M:%S.%f") -> str:
- return datetime.datetime.fromtimestamp(nano / 1e9).strftime(fmt)
+ return datetime.datetime.fromtimestamp((nano // 1000) / 1000000, tz=_cst_tz).strftime(fmt)
def _str_to_timestamp_nano(current_datetime: str, fmt="%Y-%m-%d %H:%M:%S.%f") -> int:
@@ -104,5 +148,5 @@ def _get_expire_rest_days(expire_dt, current_dt):
获取当前时间到下市时间之间的天数
expire_dt, current_dt 都以 s 为单位
"""
- delta = datetime.datetime.fromtimestamp(expire_dt).date() - datetime.datetime.fromtimestamp(current_dt).date()
+ delta = datetime.datetime.fromtimestamp(expire_dt, tz=_cst_tz).date() - datetime.datetime.fromtimestamp(current_dt, tz=_cst_tz).date()
return delta.days
diff --git a/tqsdk/lib/target_pos_task.py b/tqsdk/lib/target_pos_task.py
index 19d1cfc1..ebe4561a 100644
--- a/tqsdk/lib/target_pos_task.py
+++ b/tqsdk/lib/target_pos_task.py
@@ -4,7 +4,6 @@
import asyncio
import time
-from datetime import datetime
from asyncio import gather
from inspect import isfunction
from typing import Optional, Union, Callable
diff --git a/tqsdk/objs_not_entity.py b/tqsdk/objs_not_entity.py
index a7c81768..9722bcfd 100644
--- a/tqsdk/objs_not_entity.py
+++ b/tqsdk/objs_not_entity.py
@@ -3,7 +3,6 @@
__author__ = 'mayanqiong'
from collections import namedtuple
-from datetime import datetime
from typing import Callable, Tuple
import aiohttp
diff --git a/tqsdk/tafunc.py b/tqsdk/tafunc.py
index b452f8e6..b25cec32 100644
--- a/tqsdk/tafunc.py
+++ b/tqsdk/tafunc.py
@@ -17,7 +17,8 @@
import pandas as pd
from scipy import stats
-from tqsdk.datetime import _get_period_timestamp, _str_to_timestamp_nano, _datetime_to_timestamp_nano
+from tqsdk.datetime import _get_period_timestamp, _str_to_timestamp_nano, _datetime_to_timestamp_nano, \
+ _timestamp_nano_to_datetime, _timestamp_nano_to_str
def ref(series, n):
@@ -745,11 +746,8 @@ def time_to_str(input_time):
print(time_to_str(datetime.datetime(2019, 10, 14, 14, 26, 1))) # 将datetime.datetime时间转为%Y-%m-%d %H:%M:%S.%f 格式的str类型时间
"""
# 转为秒级时间戳
- ts = _to_ns_timestamp(input_time) / 1e9
- # 转为 %Y-%m-%d %H:%M:%S.%f 格式的 str 类型时间
- dt = datetime.datetime.fromtimestamp(ts)
- dt = dt.strftime('%Y-%m-%d %H:%M:%S.%f')
- return dt
+ ts = _to_ns_timestamp(input_time)
+ return _timestamp_nano_to_str(ts)
def time_to_datetime(input_time):
@@ -776,10 +774,8 @@ def time_to_datetime(input_time):
print(time_to_datetime("2019-10-14 14:26:01.000000")) # 将%Y-%m-%d %H:%M:%S.%f 格式的str类型时间转为datetime.datetime时间
"""
# 转为秒级时间戳
- ts = _to_ns_timestamp(input_time) / 1e9
- # 转为datetime.datetime类型
- dt = datetime.datetime.fromtimestamp(ts)
- return dt
+ ts = _to_ns_timestamp(input_time)
+ return _timestamp_nano_to_datetime(ts)
def barlast(cond):
@@ -1408,14 +1404,12 @@ def get_dividend_df(stock_dividend_ratio, cash_dividend_ratio):
"""
# 除权矩阵
stock_dividend_df = pd.DataFrame({
- "datetime": [_datetime_to_timestamp_nano(datetime.datetime.strptime(s.split(",")[0], "%Y%m%d")) for s in
- stock_dividend_ratio],
+ "datetime": [_str_to_timestamp_nano(s.split(",")[0], fmt="%Y%m%d") for s in stock_dividend_ratio],
"stock_dividend": np.array([float(s.split(",")[1]) for s in stock_dividend_ratio])
})
# 除息矩阵
cash_dividend_df = pd.DataFrame({
- "datetime": [_datetime_to_timestamp_nano(datetime.datetime.strptime(s.split(",")[0], "%Y%m%d")) for s in
- cash_dividend_ratio],
+ "datetime": [_str_to_timestamp_nano(s.split(",")[0], fmt="%Y%m%d") for s in cash_dividend_ratio],
"cash_dividend": [float(s.split(",")[1]) for s in cash_dividend_ratio]
})
# 除权除息矩阵
diff --git a/tqsdk/tools/downloader.py b/tqsdk/tools/downloader.py
index 2d6a6141..775b11b8 100644
--- a/tqsdk/tools/downloader.py
+++ b/tqsdk/tools/downloader.py
@@ -13,10 +13,9 @@
import pandas
from tqsdk.api import TqApi
-from tqsdk.channel import TqChan
-from tqsdk.datetime import _get_trading_day_start_time, _get_trading_day_end_time, _datetime_to_timestamp_nano
+from tqsdk.datetime import _cst_tz, _convert_user_input_to_nano
from tqsdk.diff import _get_obj
-from tqsdk.tafunc import get_dividend_df, get_dividend_factor
+from tqsdk.tafunc import get_dividend_df
from tqsdk.utils import _generate_uuid
try:
@@ -55,9 +54,15 @@ def __init__(self, api: TqApi, symbol_list: Union[str, List[str]], dur_sec: int,
dur_sec (int): 数据周期,以秒为单位。例如: 1分钟线为60,1小时线为3600,日线为86400,Tick数据为0
- start_dt (date/datetime): 起始时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点
+ start_dt (date/datetime): 起始时间
+ * date: 指的是交易日
- end_dt (date/datetime): 结束时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点
+ * datetime: 指的是具体时间点,如果没有指定时区信息,则默认为北京时间
+
+ end_dt (date/datetime): 结束时间
+ * date: 指的是交易日
+
+ * datetime: 指的是具体时间点,如果没有指定时区信息,则默认为北京时间
csv_file_name (str/StreamWriter): [必填]输出方式:
* str : 输出 csv 的文件名
@@ -98,14 +103,7 @@ def __init__(self, api: TqApi, symbol_list: Union[str, List[str]], dur_sec: int,
self._api = api
if not self._api._auth._has_feature("tq_dl"):
raise Exception("您的账户不支持下载历史数据功能,需要购买后才能使用。升级网址:https://www.shinnytech.com/tqsdk_professional/")
- if isinstance(start_dt, datetime):
- self._start_dt_nano = _datetime_to_timestamp_nano(start_dt)
- else:
- self._start_dt_nano = _get_trading_day_start_time(_datetime_to_timestamp_nano(datetime(start_dt.year, start_dt.month, start_dt.day)))
- if isinstance(end_dt, datetime):
- self._end_dt_nano = _datetime_to_timestamp_nano(end_dt)
- else:
- self._end_dt_nano = _get_trading_day_end_time(_datetime_to_timestamp_nano(datetime(end_dt.year, end_dt.month, end_dt.day)))
+ self._start_dt_nano, self._end_dt_nano = _convert_user_input_to_nano(start_dt, end_dt)
self._current_dt_nano = self._start_dt_nano
self._symbol_list = symbol_list if isinstance(symbol_list, list) else [symbol_list]
# 下载合约超时时间(默认 30s),已下市的没有交易的合约,超时时间可以设置短一点(2s),用户不希望自己的程序因为没有下载到数据而中断
@@ -403,5 +401,6 @@ def _get_value(obj, key, price_decs):
@staticmethod
def _nano_to_str(nano):
- dt = datetime.fromtimestamp(nano // 1000000000)
+ # 这里为了保留 nano 精度,没有用 datetime._timestamp_nano_to_str
+ dt = datetime.fromtimestamp(nano // 1000000000, tz=_cst_tz)
return "%d-%02d-%02d %02d:%02d:%02d.%09d" % (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, int(nano) % 1000000000)
diff --git a/tqsdk/tradeable/sim/trade_base.py b/tqsdk/tradeable/sim/trade_base.py
index 73d2b1e8..1709f932 100644
--- a/tqsdk/tradeable/sim/trade_base.py
+++ b/tqsdk/tradeable/sim/trade_base.py
@@ -5,7 +5,6 @@
import math
from abc import abstractmethod
-from datetime import datetime
from typing import Callable
from tqsdk.datetime import _is_in_trading_time, _str_to_timestamp_nano
diff --git a/tqsdk/tradeable/sim/utils.py b/tqsdk/tradeable/sim/utils.py
index ab2cb6a9..ad5958fd 100644
--- a/tqsdk/tradeable/sim/utils.py
+++ b/tqsdk/tradeable/sim/utils.py
@@ -2,9 +2,7 @@
# -*- coding: utf-8 -*-
__author__ = 'mayanqiong'
-from datetime import datetime
-
-from tqsdk.datetime import _get_trading_day_from_timestamp, _get_trade_timestamp
+from tqsdk.datetime import _get_trading_day_from_timestamp, _get_trade_timestamp, _timestamp_nano_to_str
TRADING_DAYS_OF_YEAR = 250
@@ -101,7 +99,7 @@ def _get_dividend_ratio(quote):
def _get_dividend_ratio_by_dt(dividend_list: list, timestamp: int) -> float:
# 从分红/送股列表中找到指定的数据返回
# ['20181102,0.400000', '20200624,0.400000', '20210716,0.400000'] '20210716'
- dt = datetime.fromtimestamp(timestamp / 1000000000).strftime('%Y%m%d') # 转为 str 格式
+ dt = _timestamp_nano_to_str(timestamp, fmt='%Y%m%d')
for item in dividend_list:
if item[:8] == dt:
return float(item[9:])
diff --git a/tqsdk/utils_symbols.py b/tqsdk/utils_symbols.py
index 636595fe..de5a7816 100644
--- a/tqsdk/utils_symbols.py
+++ b/tqsdk/utils_symbols.py
@@ -2,8 +2,7 @@
# -*- coding: utf-8 -*-
__author__ = 'mayanqiong'
-from datetime import datetime
-
+from tqsdk.datetime import _timestamp_nano_to_datetime
from tqsdk.objs import Quote
@@ -30,9 +29,9 @@ def _symbols_to_quotes(symbols, keys=set(Quote(None).keys())):
quote[key] = underlying_quote[key]
if symbol["exchange_id"] == "CFFEX" and "last_exercise_datetime" in symbol:
if key == "delivery_year":
- quote[key] = datetime.fromtimestamp(symbol["last_exercise_datetime"] / 1e9).year
+ quote[key] = _timestamp_nano_to_datetime(symbol["last_exercise_datetime"]).year
else:
- quote[key] = datetime.fromtimestamp(symbol["last_exercise_datetime"] / 1e9).month
+ quote[key] = _timestamp_nano_to_datetime(symbol["last_exercise_datetime"]).month
for k in quotes:
if quotes[k].get("ins_class", "") == "COMBINE":
# 为组合合约补充 volume_multiple
@@ -61,9 +60,9 @@ def _convert_symbol_to_quote(symbol, keys):
elif key == "last_exercise_datetime" and symbol.get("last_exercise_datetime"):
quote["last_exercise_datetime"] = symbol["last_exercise_datetime"] / 1e9
elif key == "exercise_year" and symbol.get("last_exercise_datetime"):
- quote["exercise_year"] = datetime.fromtimestamp(symbol["last_exercise_datetime"] / 1e9).year
+ quote["exercise_year"] = _timestamp_nano_to_datetime(symbol["last_exercise_datetime"]).year
elif key == "exercise_month" and symbol.get("last_exercise_datetime"):
- quote["exercise_month"] = datetime.fromtimestamp(symbol["last_exercise_datetime"] / 1e9).month
+ quote["exercise_month"] = _timestamp_nano_to_datetime(symbol["last_exercise_datetime"]).month
elif key == "pre_settlement" and "settlement_price" in symbol:
quote["pre_settlement"] = symbol["settlement_price"]
elif key in symbol: