diff --git a/PKG-INFO b/PKG-INFO index f24ecd5f..b59e670c 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: tqsdk -Version: 3.4.11 +Version: 3.5.0 Summary: TianQin SDK Home-page: https://www.shinnytech.com/tqsdk Author: TianQin diff --git a/doc/conf.py b/doc/conf.py index 430fc298..75faf5a7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = u'3.4.11' +version = u'3.5.0' # The full version, including alpha/beta/rc tags. -release = u'3.4.11' +release = u'3.5.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/usage/mddatas.rst b/doc/usage/mddatas.rst index 9ea6d3a6..6887901b 100644 --- a/doc/usage/mddatas.rst +++ b/doc/usage/mddatas.rst @@ -22,6 +22,7 @@ CZCE 郑州商品交易所 CFFEX 中国金融交易所 INE 上海能源中心(原油在这里) KQ 快期 (所有主连合约, 指数都归属在这里) +KQD 快期外盘主连合约 SSWE 上期所仓单 SSE 上海证券交易所 SZSE 深圳证券交易所 @@ -52,6 +53,8 @@ GFEX 广州期货交易所 KQ.m@CFFEX.IF - 中金所IF品种主连合约 KQ.i@SHFE.bu - 上期所bu品种指数 + KQD.m@CBOT.ZS - 美黄豆主连 + SSWE.CUH - 上期所仓单铜现货数据 SSE.600000 - 上交所浦发银行股票编码 diff --git a/doc/version.rst b/doc/version.rst index bcc991d7..46282e90 100644 --- a/doc/version.rst +++ b/doc/version.rst @@ -2,6 +2,14 @@ 版本变更 ============================= +3.5.0 (2024/01/18) + +* 新增:行情增加外盘主连合约,通过 ``api.query_quotes(exchange_id=['KQD'])`` 查询外盘合约 +* 新增::py:meth:`~tqsdk.TqZq` 众期账户类型,支持连接众期服务器交易 +* 优化::py:meth:`~tqsdk.TqApi.query_quotes` 函数 ins_class、exchange_id、product_id 参数支持 list +* 修复:ticks 回测时,可能出现账户结算信息为 nan 的问题 + + 3.4.11 (2024/01/03) * 优化:支持天勤在不同时区设置的操作系统上使用。tqsdk 内部时间表示全部使用北京时间。 diff --git a/setup.py b/setup.py index 8e064c51..db9a613f 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setuptools.setup( name='tqsdk', - version="3.4.11", + version="3.5.0", description='TianQin SDK', author='TianQin', author_email='tianqincn@gmail.com', diff --git a/tqsdk/__init__.py b/tqsdk/__init__.py index 563beba1..67c58519 100644 --- a/tqsdk/__init__.py +++ b/tqsdk/__init__.py @@ -4,7 +4,7 @@ name = "tqsdk" from tqsdk.api import TqApi -from tqsdk.tradeable import TqAccount, TqKq, TqKqStock, TqSim, TqSimStock +from tqsdk.tradeable import TqAccount, TqZq, TqKq, TqKqStock, TqSim, TqSimStock from tqsdk.auth import TqAuth from tqsdk.channel import TqChan from tqsdk.backtest import TqBacktest, TqReplay diff --git a/tqsdk/__version__.py b/tqsdk/__version__.py index 90fb0789..01bd03ce 100644 --- a/tqsdk/__version__.py +++ b/tqsdk/__version__.py @@ -1 +1 @@ -__version__ = '3.4.11' +__version__ = '3.5.0' diff --git a/tqsdk/api.py b/tqsdk/api.py index 21e71b67..c1d3ccf0 100644 --- a/tqsdk/api.py +++ b/tqsdk/api.py @@ -70,7 +70,7 @@ from tqsdk.risk_rule import TqRiskRule from tqsdk.ins_schema import ins_schema, basic, derivative, future, option from tqsdk.symbols import TqSymbols -from tqsdk.tradeable import TqAccount, TqKq, TqKqStock, TqSim, TqSimStock, BaseSim, BaseOtg +from tqsdk.tradeable import TqAccount, TqZq, TqKq, TqKqStock, TqSim, TqSimStock, BaseSim, BaseOtg from tqsdk.trading_status import TqTradingStatus from tqsdk.tqwebhelper import TqWebHelper from tqsdk.utils import _generate_uuid, _query_for_quote, BlockManagerUnconsolidated, _quotes_add_night, _bisect_value @@ -99,7 +99,7 @@ def __init__(self, account: Optional[Union[TqMultiAccount, UnionTradeable]] = No 创建天勤接口实例 Args: - account (None/TqAccount/TqKq/TqKqStock/TqSim/TqSimStock/TqMultiAccount): [可选]交易账号: + account (None/TqAccount/TqKq/TqKqStock/TqSim/TqSimStock/TqZq/TqMultiAccount): [可选]交易账号: * None: 账号将根据环境变量决定, 默认为 :py:class:`~tqsdk.TqSim` * :py:class:`~tqsdk.TqAccount` : 使用实盘账号, 直连行情和交易服务器, 需提供期货公司/帐号/密码 @@ -112,6 +112,8 @@ def __init__(self, account: Optional[Union[TqMultiAccount, UnionTradeable]] = No * :py:class:`~tqsdk.TqSimStock` : 使用 TqApi 自带的内部股票模拟账号 + * :py:class:`~tqsdk.TqZq` : 使用众期账号 + * :py:class:`~tqsdk.TqMultiAccount` : 多账户列表,列表中支持 :py:class:`~tqsdk.TqAccount`、:py:class:`~tqsdk.TqKq`、\ :py:class:`~tqsdk.TqKqStock`、:py:class:`~tqsdk.TqSim` 和 :py:class:`~tqsdk.TqSimStock` 中的 0 至 N 个或者组合 @@ -2317,13 +2319,13 @@ def query_symbol_ranking(self, symbol: str, ranking_type: str, days: int = 1, st raise TqTimeoutError(f"获取 {symbol}, {ranking_type} 持仓排名信息信息超时,请检查客户端及网络是否正常") return df - def query_quotes(self, ins_class: str = None, exchange_id: str = None, product_id: str = None, expired: bool = None, - has_night: bool = None) -> SymbolList: + def query_quotes(self, ins_class: Union[str, List[str]] = None, exchange_id: Union[str, List[str]] = None, + product_id: Union[str, List[str]] = None, expired: bool = None, has_night: bool = None) -> SymbolList: """ 根据相应的参数发送合约服务请求查询,并返回查询结果 Args: - ins_class (str): [可选] 合约类型 + ins_class (str / list of str): [可选] 合约类型 * FUTURE: 期货 * CONT: 主连 * COMBINE: 组合 @@ -2331,7 +2333,7 @@ def query_quotes(self, ins_class: str = None, exchange_id: str = None, product_i * OPTION: 期权 * STOCK: 股票 - exchange_id (str): [可选] 交易所 + exchange_id (str / list of str): [可选] 交易所 * CFFEX: 中金所 * SHFE: 上期所 * DCE: 大商所 @@ -2339,8 +2341,9 @@ def query_quotes(self, ins_class: str = None, exchange_id: str = None, product_i * INE: 能源交易所(原油) * SSE: 上交所 * SZSE: 深交所 + * KQD: 外盘主连 - product_id (str): [可选] 品种(股票、期权不能通过 product_id 筛选查询) + product_id (str / list of str): [可选] 品种(股票、期权不能通过 product_id 筛选查询) expired (bool): [可选] 是否已下市 @@ -2364,6 +2367,9 @@ def query_quotes(self, ins_class: str = None, exchange_id: str = None, product_i ls = api.query_quotes(ins_class="FUTURE", product_id="au") print(ls) # au 品种的全部合约,包括已下市以及未下市合约 + ls = api.query_quotes(ins_class=["FUTURE"], product_id=["au", "cu"], expired=False) + print(ls) # au、cu 品种的全部未下市合约合约 + ls = api.query_quotes(ins_class="INDEX", product_id="au") print(ls) # au 品种指数合约 @@ -2392,17 +2398,17 @@ def query_quotes(self, ins_class: str = None, exchange_id: str = None, product_i if ins_class is not None: if ins_class == "": raise Exception("ins_class 参数不能为空字符串。") - variables["class_"] = [ins_class] + variables["class_"] = [ins_class] if isinstance(ins_class, str) else ins_class if exchange_id is not None: if exchange_id == "": raise Exception("exchange_id 参数不能为空字符串。") # 如果是主连和指数,请求全部,在客户端区分交易所 if ins_class not in ["INDEX", "CONT"] or exchange_id not in ["CFFEX", "SHFE", "DCE", "CZCE", "INE"]: - variables["exchange_id"] = [exchange_id] + variables["exchange_id"] = [exchange_id] if isinstance(exchange_id, str) else exchange_id if product_id is not None: if product_id == "": raise Exception("product_id 参数不能为空字符串。") - variables["product_id"] = [product_id] + variables["product_id"] = [product_id] if isinstance(product_id, str) else product_id if expired is not None: variables["expired"] = expired if has_night is not None: diff --git a/tqsdk/auth.py b/tqsdk/auth.py index d5b7c0bf..cd7963a6 100644 --- a/tqsdk/auth.py +++ b/tqsdk/auth.py @@ -148,7 +148,7 @@ def _has_account(self, account): def _has_md_grants(self, symbol): symbol_list = symbol if isinstance(symbol, list) else [symbol] for symbol in symbol_list: - if symbol.split('.', 1)[0] in ["SHFE", "DCE", "CZCE", "INE", "CFFEX", "KQ", "SSWE", "GFEX"] and self._has_feature("futr"): + if symbol.split('.', 1)[0] in ["SHFE", "DCE", "CZCE", "INE", "CFFEX", "KQ", "KQD", "SSWE", "GFEX"] and self._has_feature("futr"): continue elif symbol.split('.', 1)[0] in ["CSI", "SSE", "SZSE"] and self._has_feature("sec"): continue diff --git a/tqsdk/multiaccount.py b/tqsdk/multiaccount.py index 69c596fa..e9e93760 100644 --- a/tqsdk/multiaccount.py +++ b/tqsdk/multiaccount.py @@ -8,7 +8,7 @@ from shinny_structlog import ShinnyLoggerAdapter from tqsdk.channel import TqChan -from tqsdk.tradeable import TqAccount, TqKq, TqKqStock, TqSim, TqSimStock, BaseSim +from tqsdk.tradeable import TqAccount, TqKq, TqKqStock, TqSim, TqSimStock, BaseSim, TqZq from tqsdk.tradeable.mixin import StockMixin @@ -27,12 +27,12 @@ class TqMultiAccount(object): """ - def __init__(self, accounts: Optional[List[Union[TqAccount, TqKq, TqKqStock, TqSim, TqSimStock]]] = None): + def __init__(self, accounts: Optional[List[Union[TqAccount, TqKq, TqKqStock, TqSim, TqSimStock, TqZq]]] = None): """ 创建 TqMultiAccount 实例 Args: - accounts (List[Union[TqAccount, TqKq, TqKqStock, TqSim, TqSimStock]]): [可选] 多账户列表, 若未指定任何账户, 则为 [TqSim()] + accounts (List[Union[TqAccount, TqKq, TqKqStock, TqSim, TqSimStock, TqZq]]): [可选] 多账户列表, 若未指定任何账户, 则为 [TqSim()] Example1:: diff --git a/tqsdk/tradeable/__init__.py b/tqsdk/tradeable/__init__.py index 9e51d4de..16b35ba7 100644 --- a/tqsdk/tradeable/__init__.py +++ b/tqsdk/tradeable/__init__.py @@ -5,6 +5,6 @@ from tqsdk.tradeable.otg.base_otg import BaseOtg -from tqsdk.tradeable.otg import TqAccount, TqKq, TqKqStock +from tqsdk.tradeable.otg import TqAccount, TqZq, TqKq, TqKqStock from tqsdk.tradeable.sim.basesim import BaseSim from tqsdk.tradeable.sim import TqSim, TqSimStock diff --git a/tqsdk/tradeable/otg/__init__.py b/tqsdk/tradeable/otg/__init__.py index edea5acd..d40ce95a 100644 --- a/tqsdk/tradeable/otg/__init__.py +++ b/tqsdk/tradeable/otg/__init__.py @@ -4,4 +4,5 @@ __author__ = 'mayanqiong' from tqsdk.tradeable.otg.tqaccount import TqAccount +from tqsdk.tradeable.otg.tqzq import TqZq from tqsdk.tradeable.otg.tqkq import TqKq, TqKqStock diff --git a/tqsdk/tradeable/otg/tqzq.py b/tqsdk/tradeable/otg/tqzq.py new file mode 100644 index 00000000..b9241311 --- /dev/null +++ b/tqsdk/tradeable/otg/tqzq.py @@ -0,0 +1,36 @@ +#!usr/bin/env python3 +# -*- coding:utf-8 -*- +__author__ = 'yanqiong' + +import hashlib + +from tqsdk.tradeable.otg.base_otg import BaseOtg +from tqsdk.tradeable.mixin import FutureMixin + + +class TqZq(BaseOtg, FutureMixin): + """众期账户类""" + + def __init__(self, account_id: str, password: str, td_url: str) -> None: + """ + 创建众期账户实例 + + Args: + account_id (str): 帐号 + + password (str): 密码 + + td_url (str): 众期交易服务器地址, eg: "ws://1.2.3.4:8765/" + + Example1:: + + from tqsdk import TqApi, TqZq + account = TqZq(user_name="众期账户", password="众期密码", td_url="众期柜台地址") + api = TqApi(account, auth=TqAuth("快期账户", "账户密码")) + + """ + super(TqZq, self).__init__(broker_id="", account_id=account_id, password=password, td_url=td_url) + + def _get_account_key(self): + s = self._account_id + self._td_url + return hashlib.md5(s.encode('utf-8')).hexdigest() diff --git a/tqsdk/tradeable/sim/trade_base.py b/tqsdk/tradeable/sim/trade_base.py index 1709f932..a77543ed 100644 --- a/tqsdk/tradeable/sim/trade_base.py +++ b/tqsdk/tradeable/sim/trade_base.py @@ -168,6 +168,12 @@ def _match_order(self, order, symbol, position, quote, underlying_quote=None): if last_msg == "全部成交": trade = self._generate_trade(order, quote, price) self._trades.append(trade) + # 用 ticks 回测某些期权合约的时候,position['last_price'] 初始值可能是 nan, + # 当委托单成交时,由于 position['last_price'] 为 nan 会导致后续持仓市值、保证金等计算得到 nan + # 所以这里如果发现价格时 nan,就用当前成交价记为 position['last_price'] + # 后续在 update_quotes 时,会更新 position['last_price'],因为 update_quotes 会跳过无效行情,所以这里不会再变为 nan + if math.isnan(position['last_price']): + position['last_price'] = price self._on_order_traded(order, trade, symbol, position, quote, underlying_quote) else: self._on_order_failed(symbol, order) diff --git a/tqsdk/tradeable/sim/trade_future.py b/tqsdk/tradeable/sim/trade_future.py index 804bfbe7..c81a971f 100644 --- a/tqsdk/tradeable/sim/trade_future.py +++ b/tqsdk/tradeable/sim/trade_future.py @@ -388,13 +388,21 @@ def _on_order_failed(self, symbol, order): def _on_update_quotes(self, symbol, position, quote, underlying_quote): # 调整持仓保证金和盈亏 - underlying_last_price = underlying_quote["last_price"] if underlying_quote else float('nan') + if underlying_quote is None: + underlying_last_price = float("nan") + else: + # 期权,underlying_last_price 用于计算期权保证金,必须不是 nan,这里依次选取以下价格中第一个不为 nan 的 + # 标的合约最新价,前一次持仓中记录的不为 nan 的标的最新价,期权行权价 + if math.isnan(underlying_quote["last_price"]): + underlying_last_price = quote["strike_price"] if math.isnan(position["underlying_last_price"]) \ + else position["underlying_last_price"] + else: + underlying_last_price = underlying_quote["last_price"] future_margin = _get_future_margin(quote) if position["volume_long"] > 0 or position["volume_short"] > 0: if position["last_price"] != quote["last_price"] \ or (math.isnan(future_margin) or future_margin != position["future_margin"]) \ - or (underlying_quote and ( - math.isnan(underlying_last_price) or underlying_last_price != position["underlying_last_price"])): + or (underlying_quote and underlying_last_price != position["underlying_last_price"]): self._adjust_position_account(symbol, quote, underlying_quote, pre_last_price=position["last_price"], last_price=quote["last_price"],