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: