diff --git a/PKG-INFO b/PKG-INFO index c3f692ae..20594945 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: tqsdk -Version: 2.9.2 +Version: 2.9.3 Summary: TianQin SDK Home-page: https://www.shinnytech.com/tqsdk Author: TianQin diff --git a/doc/conf.py b/doc/conf.py index a2997077..4ee31fb0 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -48,9 +48,9 @@ # built documents. # # The short X.Y version. -version = u'2.9.2' +version = u'2.9.3' # The full version, including alpha/beta/rc tags. -release = u'2.9.2' +release = u'2.9.3' # 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 248fd75d..030b14aa 100644 --- a/doc/profession.rst +++ b/doc/profession.rst @@ -61,9 +61,9 @@ TqSdk 免费版本提供全部的期货、商品/金融期权和上证50、沪 其他相关函数 ------------------------------------------------- - :py:meth:`~tqsdk.api.TqApi.query_symbol_ranking`交易所每日成交持仓排名 + :py:meth:`~tqsdk.api.TqApi.query_symbol_ranking` 交易所每日成交持仓排名 - :py:meth:`~tqsdk.api.TqApi.get_kline_data_series`以起始日期获取 Dataframe 格式的 kline 数据 + :py:meth:`~tqsdk.api.TqApi.get_kline_data_series` 以起始日期获取 Dataframe 格式的 kline 数据 :py:meth:`~tqsdk.api.TqApi.get_trading_status` 获取指定合约的交易状态,帮助用户实现开盘/跨小节抢单 diff --git a/doc/quickstart.rst b/doc/quickstart.rst index 970ba084..58f04694 100644 --- a/doc/quickstart.rst +++ b/doc/quickstart.rst @@ -269,7 +269,7 @@ klines是一个pandas.DataFrame对象. 跟 api.get_quote() 一样, api.get_kline 刚刚注册完成的信易账户的【手机号】/【邮箱地址】/【用户名】和【密码】可以作为 快期模拟 账号,通过 :py:class:`~tqsdk.api.TqKq` 对 auth 传入参数进行登录,这个 快期模拟 账户在快期APP、快期V3 pro 和天勤量化上是互通的 -快期模拟的资金可以通过快期APP、快期V3 pro的模拟银行进行出入金:: +快期模拟的资金可以通过快期APP、快期专业版的模拟银行进行出入金:: from tqsdk import TqApi, TqAuth, TqKq diff --git a/doc/reference/index.rst b/doc/reference/index.rst index 54c726f1..5627b8b0 100644 --- a/doc/reference/index.rst +++ b/doc/reference/index.rst @@ -17,5 +17,6 @@ TqSdk 模块参考 tqsdk.sim.rst tqsdk.backtest.rst tqsdk.algorithm.rst + tqsdk.risk_rule.rst tqsdk.tools.download.rst tqsdk.exceptions.rst diff --git a/doc/reference/tqsdk.risk_rule.rst b/doc/reference/tqsdk.risk_rule.rst new file mode 100644 index 00000000..4229271d --- /dev/null +++ b/doc/reference/tqsdk.risk_rule.rst @@ -0,0 +1,6 @@ +.. _tqsdk.risk_rule: + +tqsdk.risk_rule - 风控类模块 +------------------------------------------------------------------ +.. automodule:: tqsdk.risk_rule + :members: diff --git a/doc/usage/trade.rst b/doc/usage/trade.rst index 3e926eb5..31b9d3b6 100644 --- a/doc/usage/trade.rst +++ b/doc/usage/trade.rst @@ -49,7 +49,7 @@ TqApi 创建成功即代表相应账户已登录成功. 如果在60秒内无法 ---------------------------------------------------- 如果您需要使用快期模拟账户进行测试,只需在创建TqApi时传入一个 :py:class:`~tqsdk.account.TqKq` 的实例,同时需要传入信易账户 :ref:`sim_trading`。 -此账户类型与快期 APP 、天勤官网论坛、快期V3使用相同的模拟账户系统:: +此账户类型与快期 APP 、天勤官网论坛、快期专业版使用相同的模拟账户系统:: from tqsdk import TqApi, TqAuth, TqKq diff --git a/doc/version.rst b/doc/version.rst index 84fc15af..169ee7d8 100644 --- a/doc/version.rst +++ b/doc/version.rst @@ -2,6 +2,13 @@ 版本变更 ============================= +2.9.3 (2021/10/28) + +* 增加::py:class:`~tqsdk.risk_rule.TqRuleOpenCountsLimit`、:py:class:`~tqsdk.risk_rule.TqRuleOpenVolumesLimit` 类, + 以及 :py:meth:`~tqsdk.api.TqApi.add_risk_rule`、:py:meth:`~tqsdk.api.TqApi.delete_risk_rule` 接口,支持本地风控功能 +* 增加::py:class:`~tqsdk.exceptions.TqRiskRuleError` 错误类型,可以捕获风控触发的错误 + + 2.9.2 (2021/10/20) * 修复:实盘账户无法使用 :py:meth:`~tqsdk.api.TqApi.get_trading_status` 接口的问题 diff --git a/setup.py b/setup.py index 4eb40327..9db144f1 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ def get_tag(self): setuptools.setup( name='tqsdk', - version="2.9.2", + version="2.9.3", description='TianQin SDK', author='TianQin', author_email='tianqincn@gmail.com', diff --git a/tqsdk/__init__.py b/tqsdk/__init__.py index 4e47d463..4f4cbdfc 100644 --- a/tqsdk/__init__.py +++ b/tqsdk/__init__.py @@ -8,7 +8,7 @@ from tqsdk.auth import TqAuth from tqsdk.channel import TqChan from tqsdk.backtest import TqBacktest, TqReplay -from tqsdk.exceptions import BacktestFinished, TqTimeoutError +from tqsdk.exceptions import BacktestFinished, TqTimeoutError, TqRiskRuleError from tqsdk.lib import TargetPosScheduler, TargetPosTask, InsertOrderUntilAllTradedTask, InsertOrderTask, TqNotify from tqsdk.sim import TqSim from tqsdk.multiaccount import TqMultiAccount diff --git a/tqsdk/__version__.py b/tqsdk/__version__.py index c0d35b3f..4ef1be1f 100644 --- a/tqsdk/__version__.py +++ b/tqsdk/__version__.py @@ -1 +1 @@ -__version__ = '2.9.2' +__version__ = '2.9.3' diff --git a/tqsdk/api.py b/tqsdk/api.py index 877163b4..a976fd01 100644 --- a/tqsdk/api.py +++ b/tqsdk/api.py @@ -74,6 +74,8 @@ from tqsdk.objs import SecurityAccount, SecurityOrder, SecurityTrade, SecurityPosition from tqsdk.objs_not_entity import QuoteList, TqDataFrame, TqSymbolDataFrame, SymbolList, SymbolLevelList, \ TqSymbolRankingDataFrame, TqOptionGreeksDataFrame +from tqsdk.risk_manager import TqRiskManager +from tqsdk.risk_rule import TqRiskRule from tqsdk.sim import TqSim from tqsdk.symbols import TqSymbols from tqsdk.trading_status import TqTradingStatus @@ -243,6 +245,7 @@ def __init__(self, account: Union[TqMultiAccount, TqAccount, TqSim, None] = None self._td_url = _td_url # 内部关键数据 + self._risk_manager = TqRiskManager() self._requests = { "trading_status": set(), "quotes": set(), @@ -376,13 +379,13 @@ def get_quote(self, symbol: str) -> Quote: 注意: 1. 在 tqsdk 还没有收到行情数据包时, 此对象中各项内容为 NaN 或 0 2. 天勤接口从0.8版本开始,合约代码格式变更为 交易所代码.合约代码 的格式. 可用的交易所代码如下: - * CFFEX: 中金所 - * SHFE: 上期所 - * DCE: 大商所 - * CZCE: 郑商所 - * INE: 能源交易所(原油) - * SSE: 上交所 - * SZSE: 深交所 + * CFFEX: 中金所 + * SHFE: 上期所 + * DCE: 大商所 + * CZCE: 郑商所 + * INE: 能源交易所(原油) + * SSE: 上交所 + * SZSE: 深交所 Example1:: @@ -443,13 +446,13 @@ def get_quote_list(self, symbols: List[str]) -> List[Quote]: 注意: 1. 在 tqsdk 还没有收到行情数据包时, 此对象中各项内容为 NaN 或 0 2. 天勤接口从0.8版本开始,合约代码格式变更为 交易所代码.合约代码 的格式. 可用的交易所代码如下: - * CFFEX: 中金所 - * SHFE: 上期所 - * DCE: 大商所 - * CZCE: 郑商所 - * INE: 能源交易所(原油) - * SSE: 上交所 - * SZSE: 深交所 + * CFFEX: 中金所 + * SHFE: 上期所 + * DCE: 大商所 + * CZCE: 郑商所 + * INE: 能源交易所(原油) + * SSE: 上交所 + * SZSE: 深交所 Example:: @@ -1082,6 +1085,37 @@ def query_his_cont_quotes(self, symbol: Union[str, List[str]], n: int = 200): df.reset_index(inplace=True, drop=True) return df + # ---------------------------------------------------------------------- + def add_risk_rule(self, rule: TqRiskRule): + """ + 添加一项风控规则实例,此接口为 TqSdk 专业版提供。 + + 如需使用此功能,可以点击 `天勤量化专业版 `_ 申请试用或购买 + + Args: + rule (TqRiskRule): 风控规则实例,必须是 TqRiskRule 的子类型 + + """ + if not self._auth._has_feature("tq_lc_rk"): + raise Exception("本地风控功能仅限专业版用户使用,如需购买专业版或者申请试用,请访问 https://www.shinnytech.com/tqsdk_professional/") + if not isinstance(rule, TqRiskRule): + raise Exception("传入参数对象必须是 TqRiskRule 的类型") + self._risk_manager.append(rule) + + def delete_risk_rule(self, rule: TqRiskRule): + """ + 删除一项风控规则实例,此接口为 TqSdk 专业版提供。 + + 如需使用此功能,可以点击 `天勤量化专业版 `_ 申请试用或购买 + + Args: + rule (TqRiskRule): 风控规则实例,必须是 TqRiskRule 的子类型 + + """ + if not isinstance(rule, TqRiskRule): + raise Exception("传入参数对象必须是 TqRiskRule 的类型") + self._risk_manager.remove(rule) + # ---------------------------------------------------------------------- def insert_order(self, symbol: str, direction: str, offset: str = "", volume: int = 0, limit_price: Union[str, float, None] = None, @@ -1322,7 +1356,8 @@ def _get_insert_order_pack(self, symbol, direction, offset, volume, limit_price, msg["contingent_condition"] = "UNKNOWN" msg["volume_condition"] = "UNKNOWN" msg["time_condition"] = "UNKNOWN" - + self._risk_manager._could_insert_order(msg) + self._risk_manager._on_insert_order(msg) return msg async def _insert_order_async(self, symbol, direction, offset, volume, limit_price, advanced, order_id, @@ -2194,20 +2229,20 @@ def query_symbol_ranking(self, symbol: str, ranking_type: str, days: int = 1, st Returns: pandas.DataFrame: 本函数返回 pandas.DataFrame 实例。行数为 days * 20,每行为一条成交量/多头持仓量/空头持仓量的排名信息。返回值不会再更新。包含以下列: - * datetime (查询日期) - * symbol (合约代码,以交易所列出的期货公司名称为准) - * exchange_id (交易所) - * instrument_id (交易所内合约代码) - * broker (期货公司) - * volume (成交量) - * volume_change (成交量变化) - * volume_ranking (成交量排名) - * long_oi (多头持仓量) - * long_change (多头持仓增减量) - * long_ranking (多头持仓量排名) - * short_oi (空头持仓量) - * short_change (空头持仓增减量) - * short_ranking (空头持仓量排名) + * datetime (查询日期) + * symbol (合约代码,以交易所列出的期货公司名称为准) + * exchange_id (交易所) + * instrument_id (交易所内合约代码) + * broker (期货公司) + * volume (成交量) + * volume_change (成交量变化) + * volume_ranking (成交量排名) + * long_oi (多头持仓量) + * long_change (多头持仓增减量) + * long_ranking (多头持仓量排名) + * short_oi (空头持仓量) + * short_change (空头持仓增减量) + * short_ranking (空头持仓量排名) 注意: 1. 返回值中 datetime、symbol、exchange_id、instrument_id、broker 这几列一定为有效值。其他列会根据不同的 ranking_type 参数值,可能返回 nan: diff --git a/tqsdk/exceptions.py b/tqsdk/exceptions.py index 5ab9c1d1..9f2b8c90 100644 --- a/tqsdk/exceptions.py +++ b/tqsdk/exceptions.py @@ -105,3 +105,13 @@ class TqBacktestPermissionError(Exception): def __init__(self, message): super().__init__(message) self.message = message + + +class TqRiskRuleError(Exception): + """ + 风控触发的报错 + """ + + def __init__(self, message): + super().__init__(message) + self.message = message diff --git a/tqsdk/risk_manager.py b/tqsdk/risk_manager.py new file mode 100644 index 00000000..13faabed --- /dev/null +++ b/tqsdk/risk_manager.py @@ -0,0 +1,30 @@ +#!usr/bin/env python3 +# -*- coding:utf-8 -*- + +__author__ = 'mayanqiong' + +from tqsdk.exceptions import TqRiskRuleError + + +class TqRiskManager(list): + + def append(self, rule): + if rule not in self: + super(TqRiskManager, self).append(rule) + + def remove(self, rule): + if rule in self: + super(TqRiskManager, self).remove(rule) + + def _could_insert_order(self, pack): + # 是否可以下单 + for r in self: + is_valid, err_msg = r._could_insert_order(pack) + if not is_valid: + raise TqRiskRuleError(err_msg) + return True + + def _on_insert_order(self, pack): + # 需要更新风控对象内部统计值 + for r in self: + r._on_insert_order(pack) diff --git a/tqsdk/risk_rule.py b/tqsdk/risk_rule.py new file mode 100644 index 00000000..e14be96b --- /dev/null +++ b/tqsdk/risk_rule.py @@ -0,0 +1,189 @@ +#!usr/bin/env python3 +# -*- coding:utf-8 -*- + +__author__ = 'mayanqiong' + + +from abc import abstractmethod + + +class TqRiskRule(object): + + def __init__(self, api, account=None): + # 记录必要参数, 每个风控规则都需要继承这个基类 + self._api = api + account = self._api._account._check_valid(account) + if account is None: + raise Exception(f"多账户模式下, 需要指定账户实例 account") + self._account = account + self._account_key = self._account._account_key + + @abstractmethod + def _could_insert_order(self, pack) -> (bool, str): # 是否可以下单 + pass + + @abstractmethod + def _on_insert_order(self, pack): + pass + + +class TqRuleOpenCountsLimit(TqRiskRule): + """ + 风控规则类 - 交易日内开仓次数限制。 + + 此功能为 TqSdk 专业版提供,如需使用此功能,可以点击 `天勤量化专业版 `_ 申请试用或购买 + """ + + def __init__(self, api, open_counts_limit, symbol, account=None): + """ + Args: + api (TqApi): TqApi 实例 + + open_volumes_limit (int): 交易日内开仓手数上限 + + symbol (str/list of str): 负责限制的合约代码或合约代码列表. + * str: 一个合约代码 + * list of str: 合约代码列表 + + account (TqAccount/TqKq/TqSim): [可选] 指定发送下单指令的账户实例, 多账户模式下,该参数必须指定 + + Example1:: + + from tqsdk import TqApi + from tqsdk.risk_rule import TqRuleOpenCountsLimit + + api = TqApi(auth=TqAuth("信易账户", "账户密码")) + + rule = TqRuleOpenCountsLimit(api, open_counts_limit=10, symbol="DCE.m2112") # 创建风控规则实例 + api.add_risk_rule(rule) # 添加风控规则 + + quote = api.get_quote("DCE.m2112") + try: + # 每次最新价变动,下一笔订单,直到超过开仓次数风控限制 + while True: + api.wait_update() + if api.is_changing(quote, ['last_price']): + order = api.insert_order(symbol="DCE.m2112", direction="BUY", offset="OPEN", volume=1) + while order.status != "FINISHED": + api.wait_update() + except TqRiskRuleError as e: + print('!!!', e) + api.close() + + """ + super(TqRuleOpenCountsLimit, self).__init__(api=api, account=account) + if open_counts_limit < 0: + raise Exception("参数 open_volumes_limit 必须大于 0 的数字") + self.open_counts_limit = open_counts_limit + self.symbol_list = [symbol] if isinstance(symbol, str) else symbol + self.data = {s: 0 for s in self.symbol_list} + for order_id, order in self._api._data.get('trade', {}).get(self._account_key, {}).get('orders', {}).items(): + symbol = order["exchange_id"] + "." + order["instrument_id"] + if order["offset"] == "OPEN" and symbol in self.data: + self.data[symbol] += 1 + + def _could_insert_order(self, pack) -> {bool, str}: + if pack['account_key'] == self._account_key: + symbol = pack["exchange_id"] + "." + pack["instrument_id"] + if pack["offset"] == "OPEN" and symbol in self.symbol_list: + if self.data[symbol] + 1 > self.open_counts_limit: + return False, f"触发风控规则,合约 {symbol} 开仓到达交易日内开仓次数限制 {self.open_counts_limit}, " \ + f"已下单次数 {self.data[symbol]}" + return True, "" + + def _on_insert_order(self, pack): + if pack['account_key'] == self._account_key: + symbol = pack["exchange_id"] + "." + pack["instrument_id"] + if pack["offset"] == "OPEN" and symbol in self.symbol_list: + self.data[symbol] += 1 + + +class TqRuleOpenVolumesLimit(TqRiskRule): + """ + 风控规则类 - 交易日内开仓手数限制 + + 此功能为 TqSdk 专业版提供,如需使用此功能,可以点击 `天勤量化专业版 `_ 申请试用或购买 + """ + + def __init__(self, api, open_volumes_limit, symbol, account=None): + """ + Args: + api (TqApi): TqApi 实例 + + open_volumes_limit (int): 交易日内开仓手数上限 + + symbol (str/list of str): 负责限制的合约代码或合约代码列表. + * str: 一个合约代码 + * list of str: 合约代码列表 + + account (TqAccount/TqKq/TqSim): [可选] 指定发送下单指令的账户实例, 多账户模式下,该参数必须指定 + + Example1:: + + from tqsdk import TqApi + from tqsdk.risk_rule import TqRuleOpenVolumesLimit + + api = TqApi(auth=TqAuth("信易账户", "账户密码")) + + rule = TqRuleOpenVolumesLimit(api, open_volumes_limit=10, symbol="DCE.m2112") # 创建风控规则实例 + api.add_risk_rule(rule) # 添加风控规则 + + # 下单 5 手,不会触发风控规则 + order1 = api.insert_order(symbol="DCE.m2112", direction="BUY", offset="OPEN", volume=5) + while order1.status != "FINISHED": + api.wait_update() + + # 继续下单 8 手,会触发风控规则 + order2 = api.insert_order(symbol="DCE.m2112", direction="BUY", offset="OPEN", volume=8) + while order2.status != "FINISHED": + api.wait_update() + api.close() + + + Example2:: + + + from tqsdk import TqApi, TqKq, TqRiskRuleError + from tqsdk.risk_rule import TqRuleOpenVolumesLimit + + account = TqKq() + api = TqApi(account=account, auth=TqAuth("信易账户", "账户密码")) + + rule = TqRuleOpenVolumesLimit(api, open_volumes_limit=10, symbol="DCE.m2112", account=account) # 创建风控规则实例 + api.add_risk_rule(rule) # 添加风控规则 + + try: + # 下单 11 手,触发风控规则 + order1 = api.insert_order(symbol="DCE.m2112", direction="BUY", offset="OPEN", volume=11) + while order1.status != "FINISHED": + api.wait_update() + except TqRiskRuleError as e: + print("!!!", e) + + api.close() + """ + super(TqRuleOpenVolumesLimit, self).__init__(api=api, account=account) + if open_volumes_limit < 0: + raise Exception("参数 open_volumes_limit 必须大于 0 的数字") + self.open_volumes_limit = open_volumes_limit + self.symbol_list = [symbol] if isinstance(symbol, str) else symbol + self.data = {s: 0 for s in self.symbol_list} + for order_id, order in self._api._data.get('trade', {}).get(self._account_key, {}).get('orders', {}).items(): + symbol = order["exchange_id"] + "." + order["instrument_id"] + if order["offset"] == "OPEN" and symbol in self.data: + self.data[symbol] += order["volume"] + + def _could_insert_order(self, pack) -> {bool, str}: + if pack['account_key'] == self._account_key: + symbol = pack["exchange_id"] + "." + pack["instrument_id"] + if pack["offset"] == "OPEN" and symbol in self.symbol_list: + if self.data[symbol] + pack["volume"] > self.open_volumes_limit: + return False, f"触发风控规则,合约 {symbol} 开仓到达交易日内开仓手数限制 {self.open_volumes_limit}, " \ + f"已下单手数 {self.data[symbol]}, 即将下单手数 {pack['volume']}" + return True, "" + + def _on_insert_order(self, pack): + if pack['account_key'] == self._account_key: + symbol = pack["exchange_id"] + "." + pack["instrument_id"] + if pack["offset"] == "OPEN" and symbol in self.symbol_list: + self.data[symbol] += pack["volume"]