class TargetPosTask(object, metaclass=TargetPosTaskSingleton): """目标持仓 task, 该 task 可以将指定合约调整到目标头寸""" def __init__( self, api: TqApi, symbol: str, price: Union[str, Callable[[str], Union[float, int]]] = "ACTIVE", offset_priority: str = "今昨,开", min_volume: Optional[int] = None, max_volume: Optional[int] = None, trade_chan: Optional[TqChan] = None, trade_objs_chan: Optional[TqChan] = None, account: Optional[Union[TqAccount, TqKq, TqSim]] = None) -> None: """ 创建目标持仓task实例,负责调整归属于该task的持仓 **(默认为整个账户的该合约净持仓)**. **注意:** 1. TargetPosTask 在 set_target_volume 时并不下单或撤单, 它的下单和撤单动作, 是在之后的每次 wait_update 时执行的. 因此, **需保证 set_target_volume 后还会继续调用wait_update()** 。 2. 请勿在使用 TargetPosTask 的同时使用 insert_order() 函数, 否则将导致 TargetPosTask 报错或错误下单。 3. TargetPosTask 如果同时设置 min_volume(每笔最小下单手数),max_volume(每笔最大下单的手数)两个参数,表示采用 **大单拆分模式** 下单。 在 **大单拆分模式** 下,每次下单的手数为随机生成的正整数,值介于 min_volume、max_volume 之间。 具体说明:调用 set_target_volume 后,首先会根据目标持仓手数、开平仓顺序计算出,需要平今、平昨、开仓的目标下单手数及顺序。 + 如果在调整持仓的目标下单手数小于 max_volume,则直接以目标下单手数下单。 + 如果在调整持仓的目标下单手数大于等于 max_volume,则会以 min_volume、max_volume 之间的随机手数下一笔委托单,手数全部成交后,会接着处理剩余的手数;\ 继续以随机手数下一笔委托单,全部成交后,继续处理剩余的手数,直至剩余手数小于 max_volume 时,直接以剩余手数下单。 当使用大单拆分模式下单时,必须同时填写 min_volume、max_volume,且需要满足 max_volume >= min_volume > 0。 Args: api (TqApi): TqApi实例,该task依托于指定api下单/撤单 symbol (str): 负责调整的合约代码 price (str / Callable): [可选]下单方式, 默认为 "ACTIVE"。 * "ACTIVE":对价下单,在持仓调整过程中,若下单方向为买,对价为卖一价;若下单方向为卖,对价为买一价。 * "PASSIVE":排队价下单,在持仓调整过程中,若下单方向为买,对价为买一价;若下单方向为卖,对价为卖一价。 * Callable[[str], Union[float, int]]: 函数参数为下单方向,函数返回值是下单价格。如果返回 nan,程序会抛错。 offset_priority (str): [可选]开平仓顺序,昨=平昨仓,今=平今仓,开=开仓,逗号=等待之前操作完成 对于下单指令区分平今/昨的交易所(如上期所),按照今/昨仓的数量计算是否能平今/昨仓 对于下单指令不区分平今/昨的交易所(如中金所),按照“先平当日新开仓,再平历史仓”的规则计算是否能平今/昨仓,如果这些交易所设置为"昨开"在有当日新开仓和历史仓仓的情况下,会自动跳过平昨仓进入到下一步 * "今昨,开" 表示先平今仓,再平昨仓,等待平仓完成后开仓,对于没有单向大边的品种避免了开仓保证金不足 * "今昨开" 表示先平今仓,再平昨仓,并开仓,所有指令同时发出,适合有单向大边的品种 * "昨开" 表示先平昨仓,再开仓,禁止平今仓,适合股指这样平今手续费较高的品种 * "开" 表示只开仓,不平仓,适合需要进行锁仓操作的品种 min_volume (int): [可选] **大单拆分模式下** 每笔最小下单的手数,默认不启用 **大单拆分模式** max_volume (int): [可选] **大单拆分模式下** 每笔最大下单的手数,默认不启用 **大单拆分模式** trade_chan (TqChan): [可选]成交通知channel, 当有成交发生时会将成交手数(多头为正数,空头为负数)发到该channel上 trade_objs_chan (TqChan): [可选]成交对象通知channel, 当有成交发生时会将成交对象发送到该channel上 account (TqAccount/TqKq/TqSim): [可选]指定发送下单指令的账户实例, 多账户模式下,该参数必须指定 **注意** 当 price 参数为函数类型时,该函数应该返回一个有效的价格值,应该避免返回 nan。以下为 price 参数是函数类型时的示例。 Example1:: # ... 用户代码 ... quote = api.get_quote("SHFE.cu2012") def get_price(direction): # 在 BUY 时使用买一价加一档价格,SELL 时使用卖一价减一档价格 if direction == "BUY": price = quote.bid_price1 + quote.price_tick else: price = quote.ask_price1 - quote.price_tick # 如果 price 价格是 nan,使用最新价报单 if price != price: price = quote.last_price return price target_pos = TargetPosTask(api, "SHFE.cu2012", price=get_price) # ... 用户代码 ... Example2:: # ... 用户代码 ... quote1 = api.get_quote("SHFE.cu2012") quote2 = api.get_quote("SHFE.au2012") def get_price(direction, quote): # 在 BUY 时使用买一价加一档价格,SELL 时使用卖一价减一档价格 if direction == "BUY": price = quote.bid_price1 + quote.price_tick else: price = quote.ask_price1 - quote.price_tick # 如果 price 价格是 nan,使用最新价报单 if price != price: price = quote.last_price return price target_pos1 = TargetPosTask(api, "SHFE.cu2012", price=lambda direction: get_price(direction, quote1)) target_pos2 = TargetPosTask(api, "SHFE.au2012", price=lambda direction: get_price(direction, quote2)) # ... 用户代码 ... Example3:: # 大单拆分模式用法示例 from tqsdk import TqApi, TqAuth, TargetPosTask api = TqApi(auth=TqAuth("信易账户", "账户密码")) position = api.get_position('SHFE.rb2106') # 同时设置 min_volume、max_volume 两个参数,表示使用大单拆分模式 t = TargetPosTask(api, 'SHFE.rb2106', min_volume=2, max_volume=10) t.set_target_volume(50) while True: api.wait_update() if position.pos_long == 50: break api.close() # 说明: # 以上代码使用 TqSim 交易,开始时用户没有 SHFE.cu2012 合约的任何持仓,那么在 t.set_target_volume(50) 之后应该开多仓 50 手 # 根据用户参数,下单使用大单拆分模式,每次下单手数在 2~10 之间,打印出的成交通知可能是这样的: # 2021-03-15 11:29:48 - INFO - 模拟交易成交记录 # 2021-03-15 11:29:48 - INFO - 时间: 2021-03-15 11:29:47.516138, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 7, 价格: 4687.000,手续费: 32.94 # 2021-03-15 11:29:48 - INFO - 时间: 2021-03-15 11:29:47.519699, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 8, 价格: 4687.000,手续费: 37.64 # 2021-03-15 11:29:48 - INFO - 时间: 2021-03-15 11:29:47.522848, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 10, 价格: 4687.000,手续费: 47.05 # 2021-03-15 11:29:48 - INFO - 时间: 2021-03-15 11:29:47.525617, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 8, 价格: 4687.000,手续费: 37.64 # 2021-03-15 11:29:48 - INFO - 时间: 2021-03-15 11:29:47.528151, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 7, 价格: 4687.000,手续费: 32.94 # 2021-03-15 11:29:48 - INFO - 时间: 2021-03-15 11:29:47.530930, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 7, 价格: 4687.000,手续费: 32.94 # 2021-03-15 11:29:48 - INFO - 时间: 2021-03-15 11:29:47.533515, 合约: SHFE.rb2106, 开平: OPEN, 方向: BUY, 手数: 3, 价格: 4687.000,手续费: 14.12 """ if symbol.startswith("CZCE.CJ"): raise Exception( "红枣期货不支持创建 targetpostask、twap、vwap 任务,交易所规定该品种最小开仓手数为大于等于 4 手,这些函数还未支持该规则!" ) if symbol.startswith("CZCE.ZC"): raise Exception( "动力煤期货不支持创建 targetpostask、twap、vwap 任务,交易所规定该品种最小开仓手数为大于等于 2 手,这些函数还未支持该规则!" ) super(TargetPosTask, self).__init__() self._api = api self._account = api._account._check_valid(account) self._symbol = symbol self._exchange = symbol.split(".")[0] self._offset_priority = _check_offset_priority(offset_priority) self._min_volume, self._max_volume = _check_volume_limit( min_volume, max_volume) self._price = _check_price(price) self._pos = self._account.get_position(self._symbol) self._pos_chan = TqChan(self._api, last_only=True) self._trade_chan = trade_chan self._trade_objs_chan = trade_objs_chan self._task = self._api.create_task(self._target_pos_task()) self._time_update_task = self._api.create_task( self._update_time_from_md()) # 监听行情更新并记录当时本地时间的task self._local_time_record = time.time() - 0.005 # 更新最新行情时间时的本地时间 self._local_time_record_update_chan = TqChan( self._api, last_only=True) # 监听 self._local_time_record 更新 def set_target_volume(self, volume: int) -> None: """ 设置目标持仓手数 Args: volume (int): 目标持仓手数,正数表示多头,负数表示空头,0表示空仓 Example1:: # 设置 rb1810 持仓为多头5手 from tqsdk import TqApi, TqAuth, TargetPosTask api = TqApi(auth=TqAuth("信易账户", "账户密码")) target_pos = TargetPosTask(api, "SHFE.rb1810") target_pos.set_target_volume(5) while True: # 需在 set_target_volume 后调用wait_update()以发出指令 api.wait_update() Example2:: # 多账户模式下使用 TargetPosTask from tqsdk import TqApi, TqMultiAccount, TqAuth, TargetPosTask account1 = TqAccount("H海通期货", "123456", "123456") account2 = TqAccount("H宏源期货", "654321", "123456") api = TqApi(TqMultiAccount([account1, account2]), auth=TqAuth("信易账户", "账户密码")) symbol1 = "DCE.m2105" symbol2 = "DCE.i2101" # 多账户模式下, 调仓工具需要指定账户实例 target_pos1 = TargetPosTask(api, symbol1, account=account1) target_pos2 = TargetPosTask(api, symbol2, account=account2) target_pos1.set_target_volume(30) target_pos2.set_target_volume(80) while True: api.wait_update() api.close() """ if self._task.done(): raise Exception("已经结束的 TargetPosTask 实例不可以再设置手数。") self._pos_chan.send_nowait(int(volume)) def _get_order(self, offset, vol, pending_frozen): """ 根据指定的offset和预期下单手数vol, 返回符合要求的委托单最大报单手数 :param offset: "昨" / "今" / "开" :param vol: int, <0表示SELL, >0表示BUY :return: order_offset: "CLOSE"/"CLOSETODAY"/"OPEN"; order_dir: "BUY"/"SELL"; "order_volume": >=0, 报单手数 """ if vol > 0: # 买单(增加净持仓) order_dir = "BUY" pos_all = self._pos.pos_short else: # 卖单 order_dir = "SELL" pos_all = self._pos.pos_long if offset == "昨": order_offset = "CLOSE" if self._exchange == "SHFE" or self._exchange == "INE": if vol > 0: pos_all = self._pos.pos_short_his else: pos_all = self._pos.pos_long_his frozen_volume = sum([ order.volume_left for order in self._pos.orders.values() if not order.is_dead and order.offset == order_offset and order.direction == order_dir ]) else: frozen_volume = pending_frozen + sum([ order.volume_left for order in self._pos.orders.values() if not order.is_dead and order.offset != "OPEN" and order.direction == order_dir ]) # 判断是否有未冻结的今仓手数: 若有则不平昨仓 if (self._pos.pos_short_today if vol > 0 else self._pos.pos_long_today) - frozen_volume > 0: pos_all = frozen_volume order_volume = min(abs(vol), max(0, pos_all - frozen_volume)) elif offset == "今": if self._exchange == "SHFE" or self._exchange == "INE": order_offset = "CLOSETODAY" if vol > 0: pos_all = self._pos.pos_short_today else: pos_all = self._pos.pos_long_today frozen_volume = sum([ order.volume_left for order in self._pos.orders.values() if not order.is_dead and order.offset == order_offset and order.direction == order_dir ]) else: order_offset = "CLOSE" frozen_volume = pending_frozen + sum([ order.volume_left for order in self._pos.orders.values() if not order.is_dead and order.offset != "OPEN" and order.direction == order_dir ]) pos_all = self._pos.pos_short_today if vol > 0 else self._pos.pos_long_today order_volume = min(abs(vol), max(0, pos_all - frozen_volume)) elif offset == "开": order_offset = "OPEN" order_volume = abs(vol) else: order_offset = "" order_volume = 0 return order_offset, order_dir, order_volume async def _update_time_from_md(self): """监听行情更新并记录当时本地时间的task""" try: chan = TqChan(self._api, last_only=True) self._quote = await self._api.get_quote(self._symbol) self._api.register_update_notify(self._quote, chan) # quote有更新时: 更新记录的时间 if isinstance(self._api._backtest, TqBacktest): # 回测情况下,在收到回测时间有更新的时候,也需要更新记录的时间 self._api.register_update_notify( _get_obj(self._api._data, ["_tqsdk_backtest"]), chan) async for _ in chan: self._local_time_record = time.time() - 0.005 # 更新最新行情时间时的本地时间 self._local_time_record_update_chan.send_nowait( True) # 通知记录的时间有更新 finally: await chan.close() async def _target_pos_task(self): """负责调整目标持仓的task""" all_tasks = [] try: self._quote = await self._api.get_quote(self._symbol) async for target_pos in self._pos_chan: # lib 中对于时间判断的方案: # 如果当前时间(模拟交易所时间)不在交易时间段内,则:等待直到行情更新 # 行情更新(即下一交易时段开始)后:获取target_pos最新的目标仓位, 开始调整仓位 # 如果不在可交易时间段内(回测时用 backtest 下发的时间判断,实盘使用 quote 行情判断): 等待更新 while True: if isinstance(self._api._backtest, TqBacktest): cur_timestamp = self._api._data.get( "_tqsdk_backtest", {}).get("current_dt", float("nan")) cur_dt = _timestamp_nano_to_str(cur_timestamp) time_record = float("nan") else: cur_dt = self._quote["datetime"] time_record = self._local_time_record if _is_in_trading_time(self._quote, cur_dt, time_record): break await self._local_time_record_update_chan.recv() target_pos = self._pos_chan.recv_latest( target_pos) # 获取最后一个target_pos目标仓位 # 确定调仓增减方向 delta_volume = target_pos - self._pos.pos pending_forzen = 0 for each_priority in self._offset_priority + ",": # 按不同模式的优先级顺序报出不同的offset单,股指(“昨开”)平昨优先从不平今就先报平昨,原油平今优先("今昨开")就报平今 if each_priority == ",": await gather(*[each._task for each in all_tasks]) pending_forzen = 0 all_tasks = [] continue order_offset, order_dir, order_volume = self._get_order( each_priority, delta_volume, pending_forzen) if order_volume == 0: # 如果没有则直接到下一种offset continue elif order_offset != "OPEN": pending_forzen += order_volume order_task = InsertOrderUntilAllTradedTask( self._api, self._symbol, order_dir, offset=order_offset, volume=order_volume, min_volume=self._min_volume, max_volume=self._max_volume, price=self._price, trade_chan=self._trade_chan, trade_objs_chan=self._trade_objs_chan, account=self._account) all_tasks.append(order_task) delta_volume -= order_volume if order_dir == "BUY" else -order_volume finally: # 执行 task.cancel() 时, 删除掉该 symbol 对应的 TargetPosTask 实例 # self._account 类型为 TqSim/TqKq/TqAccount,都包括 _account_key 变量 TargetPosTaskSingleton._instances.pop( self._account._account_key + "#" + self._symbol, None) await self._pos_chan.close() self._time_update_task.cancel() await asyncio.gather(*([t._task for t in all_tasks] + [self._time_update_task]), return_exceptions=True) def cancel(self): """ 取消当前 TargetPosTask 实例,会将该实例已经发出但还是未成交的委托单撤单,并且如果后续调用此实例的 set_target_volume 函数会报错。 任何时刻,每个账户下一个合约只能有一个 TargetPosTask 实例,并且其构造参数不能修改。 如果对于同一个合约要构造不同参数的 TargetPosTask 实例,需要调用 cancel 方法销毁,才能创建新的 TargetPosTask 实例 Example1:: from datetime import datetime, time from tqsdk import TqApi, TargetPosTask api = TqApi(auth=TqAuth("信易账户", "账户密码")) quote = api.get_quote("SHFE.rb2110") target_pos_passive = TargetPosTask(api, "SHFE.rb2110", price="PASSIVE") while datetime.strptime(quote.datetime, "%Y-%m-%d %H:%M:%S.%f").time() < time(14, 50): api.wait_update() # ... 策略代码 ... # 取消 TargetPosTask 实例 target_pos_passive.cancel() while not target_pos_passive.is_finished(): # 此循环等待 target_pos_passive 处理 cancel 结束 api.wait_update() # 调用wait_update(),会对已经发出但还是未成交的委托单撤单 # 创建新的 TargetPosTask 实例 target_pos_active = TargetPosTask(api, "SHFE.rb2110", price="ACTIVE") target_pos_active.set_target_volume(0) # 平所有仓位 while True: api.wait_update() # ... 策略代码 ... api.close() """ self._task.cancel() def is_finished(self) -> bool: """ 返回当前 TargetPosTask 实例是否已经结束。即如果后续调用此实例的 set_target_volume 函数会报错,此实例不会再下单或者撤单。 Returns: bool: 当前 TargetPosTask 实例是否已经结束 """ return self._task.done()
class TqReplay(object): """天勤复盘类""" def __init__(self, replay_dt: date): """ 除了传统的回测模式以外,TqSdk 提供独具特色的复盘模式,它与回测模式有以下区别 1.复盘模式为时间驱动,回测模式为事件驱动 复盘模式下,你可以指定任意一天交易日,后端行情服务器会传输用户订阅合约的当天的所有历史行情数据,重演当天行情,而在回测模式下,我们根据用户订阅的合约周期数据来进行推送 因此在复盘模式下K线更新和实盘一模一样,而回测模式下就算订阅了 Tick 数据,回测中任意周期 K 线最后一根的 close 和其他数据也不会随着 Tick 更新而更新,而是随着K线频率生成和结束时更新一次 2.复盘和回测的行情速度 因为两者的驱动机制不同,回测会更快,但是我们在复盘模式下也提供行情速度调节功能,可以结合web_gui来实现 3.复盘目前只支持单日复盘 因为复盘提供对应合约当日全部历史行情数据,对后端服务器会有较大压力,目前只支持复盘模式下选择单日进行复盘 Args: replay_dt (date): 指定复盘交易日 """ if isinstance(replay_dt, date): self._replay_dt = replay_dt else: raise Exception("复盘时间(dt)类型 %s 错误, 请检查 dt 数据类型是否填写正确" % (type(replay_dt))) if self._replay_dt.weekday() >= 5: # 0~6, 检查周末[5,6] 提前抛错退出 raise Exception("无法创建复盘服务器,请检查复盘日期后重试。") self._default_speed = 1 self._api = None def _create_server(self, api): self._api = api self._logger = api._logger.getChild("TqReplay") # 调试信息输出 self._logger.debug('replay prepare', replay_dt=self._replay_dt) session = self._prepare_session() self._session_url = "http://%s:%d/t/rmd/replay/session/%s" % ( session["ip"], session["session_port"], session["session"]) self._ins_url = "http://%s:%d/t/rmd/replay/session/%s/symbol" % ( session["ip"], session["session_port"], session["session"]) self._md_url = "ws://%s:%d/t/rmd/front/mobile" % ( session["ip"], session["gateway_web_port"]) self._server_status = None self._server_status = self._wait_server_status("running", 60) if self._server_status == "running": self._logger.debug('replay start successed', replay_dt=self._replay_dt) return self._ins_url, self._md_url else: self._logger.debug('replay start failed', replay_dt=self._replay_dt) raise Exception("无法创建复盘服务器,请检查复盘日期后重试。") async def _run(self): try: self._send_chan = TqChan(self._api) self._send_chan.send_nowait({ "aid": "ratio", "speed": self._default_speed }) _senddata_task = self._api.create_task(self._senddata_handler()) while True: await self._send_chan.send({"aid": "heartbeat"}) await asyncio.sleep(30) finally: await self._send_chan.close() _senddata_task.cancel() await asyncio.gather(_senddata_task, return_exceptions=True) def _prepare_session(self): create_session_url = "http://replay.api.shinnytech.com/t/rmd/replay/create_session" response = requests.post( create_session_url, headers=self._api._base_headers, data=json.dumps({'dt': self._replay_dt.strftime("%Y%m%d")}), timeout=5) if response.status_code == 200: return json.loads(response.content) else: raise Exception("创建复盘服务器失败,请检查复盘日期后重试。") def _wait_server_status(self, target_status, timeout): """等服务器状态为 target_status,超时时间 timeout 秒""" deadline = time.time() + timeout server_status = self._get_server_status() while deadline > time.time(): if target_status == server_status: break else: time.sleep(1) server_status = self._get_server_status() return server_status def _get_server_status(self): try: response = requests.get(self._session_url, headers=self._api._base_headers, timeout=5) if response.status_code == 200: return json.loads(response.content)["status"] else: raise Exception("无法创建复盘服务器,请检查复盘日期后重试。") except requests.exceptions.ConnectionError as e: # 刚开始 _session_url 还不能访问的时候~ return None async def _senddata_handler(self): try: session = aiohttp.ClientSession(headers=self._api._base_headers) async for data in self._send_chan: await session.post(self._session_url, data=json.dumps(data)) finally: await session.post(self._session_url, data=json.dumps({"aid": "terminate"})) await session.close() def set_replay_speed(self, speed: float = 10.0) -> None: """ 调整复盘服务器行情推进速度 Args: speed (float): 复盘服务器行情推进速度, 默认为 10.0 Example:: from datetime import date from tqsdk import TqApi, TqAuth, TqReplay replay = TqReplay(date(2020, 9, 10)) api = TqApi(backtest=replay, auth=("信易账户,账户密码")) replay.set_replay_speed(3.0) quote = api.get_quote("SHFE.cu2012") while True: api.wait_update() if api.is_changing(quote): print("最新价", quote.datetime, quote.last_price) """ if self._api: self._send_chan.send_nowait({"aid": "ratio", "speed": speed}) else: # _api 未初始化,只记录用户设定的速度,在复盘服务器启动完成后,发动请求 self._default_speed = speed
class TargetPosTask(object, metaclass=TargetPosTaskSingleton): """目标持仓 task, 该 task 可以将指定合约调整到目标头寸""" def __init__(self, api: TqApi, symbol: str, price: str = "ACTIVE", offset_priority: str = "今昨,开", trade_chan: Optional[TqChan] = None) -> None: """ 创建目标持仓task实例,负责调整归属于该task的持仓 **(默认为整个账户的该合约净持仓)**. **注意:** 1. TargetPosTask 在 set_target_volume 时并不下单或撤单, 它的下单和撤单动作, 是在之后的每次 wait_update 时执行的. 因此, **需保证 set_target_volume 后还会继续调用wait_update()** 。 2. 请勿在使用 TargetPosTask 的同时使用 insert_order() 函数, 否则将导致 TargetPosTask 报错或错误下单。 Args: api (TqApi): TqApi实例,该task依托于指定api下单/撤单 symbol (str): 负责调整的合约代码 price (str): [可选]下单方式, ACTIVE=对价下单, PASSIVE=挂价下单. * 在持仓调整过程中,若下单方向为买: 对价为卖一价, 挂价为买一价 * 在持仓调整过程中,若下单方向为卖: 对价为买一价, 挂价为卖一价 offset_priority (str): [可选]开平仓顺序,昨=平昨仓,今=平今仓,开=开仓,逗号=等待之前操作完成 对于下单指令区分平今/昨的交易所(如上期所),按照今/昨仓的数量计算是否能平今/昨仓 对于下单指令不区分平今/昨的交易所(如中金所),按照“先平当日新开仓,再平历史仓”的规则计算是否能平今/昨仓 * "今昨,开" 表示先平今仓,再平昨仓,等待平仓完成后开仓,对于没有单向大边的品种避免了开仓保证金不足 * "今昨开" 表示先平今仓,再平昨仓,并开仓,所有指令同时发出,适合有单向大边的品种 * "昨开" 表示先平昨仓,再开仓,禁止平今仓,适合股指这样平今手续费较高的品种 trade_chan (TqChan): [可选]成交通知channel, 当有成交发生时会将成交手数(多头为正数,空头为负数)发到该channel上 """ super(TargetPosTask, self).__init__() self._api = api if symbol not in api._data.get("quotes", {}): raise Exception("代码 %s 不存在, 请检查合约代码是否填写正确" % (symbol)) self._symbol = symbol self._exchange = symbol.split(".")[0] if price not in ("ACTIVE", "PASSIVE"): raise Exception("下单方式(price) %s 错误, 请检查 price 参数是否填写正确" % (price)) self._price = price if len( offset_priority.replace(",", "").replace("今", "", 1).replace( "昨", "", 1).replace("开", "", 1)) > 0: raise Exception( "开平仓顺序(offset_priority) %s 错误, 请检查 offset_priority 参数是否填写正确" % (offset_priority)) self._offset_priority = offset_priority self._pos = self._api.get_position(self._symbol) self._pos_chan = TqChan(self._api, last_only=True) self._trade_chan = trade_chan if trade_chan is not None else TqChan( self._api) self._task = self._api.create_task(self._target_pos_task()) self._quote = self._api.get_quote(self._symbol) self._time_update_task = self._api.create_task( self._update_time_from_md()) # 监听行情更新并记录当时本地时间的task self._local_time_record = time.time() - 0.005 # 更新最新行情时间时的本地时间 self._local_time_record_update_chan = TqChan( self._api, last_only=True) # 监听 self._local_time_record 更新 def set_target_volume(self, volume: int) -> None: """ 设置目标持仓手数 Args: volume (int): 目标持仓手数,正数表示多头,负数表示空头,0表示空仓 Example:: # 设置 rb1810 持仓为多头5手 from tqsdk import TqApi, TargetPosTask api = TqApi() target_pos = TargetPosTask(api, "SHFE.rb1810") target_pos.set_target_volume(5) while True: # 需在 set_target_volume 后调用wait_update()以发出指令 api.wait_update() """ self._pos_chan.send_nowait(int(volume)) def _get_order(self, offset, vol, pending_frozen): """ 根据指定的offset和预期下单手数vol, 返回符合要求的委托单最大报单手数 :param offset: "昨" / "今" / "开" :param vol: int, <0表示SELL, >0表示BUY :return: order_offset: "CLOSE"/"CLOSETODAY"/"OPEN"; order_dir: "BUY"/"SELL"; "order_volume": >=0, 报单手数 """ if vol > 0: # 买单(增加净持仓) order_dir = "BUY" pos_all = self._pos.pos_short else: # 卖单 order_dir = "SELL" pos_all = self._pos.pos_long if offset == "昨": order_offset = "CLOSE" if self._exchange == "SHFE" or self._exchange == "INE": if vol > 0: pos_all = self._pos.pos_short_his else: pos_all = self._pos.pos_long_his frozen_volume = sum([ order.volume_left for order in self._pos.orders.values() if not order.is_dead and order.offset == order_offset and order.direction == order_dir ]) else: frozen_volume = pending_frozen + sum([ order.volume_left for order in self._pos.orders.values() if not order.is_dead and order.offset != "OPEN" and order.direction == order_dir ]) # 判断是否有未冻结的今仓手数: 若有则不平昨仓 if (self._pos.pos_short_today if vol > 0 else self._pos.pos_long_today) - frozen_volume > 0: pos_all = frozen_volume order_volume = min(abs(vol), max(0, pos_all - frozen_volume)) elif offset == "今": if self._exchange == "SHFE" or self._exchange == "INE": order_offset = "CLOSETODAY" if vol > 0: pos_all = self._pos.pos_short_today else: pos_all = self._pos.pos_long_today frozen_volume = sum([ order.volume_left for order in self._pos.orders.values() if not order.is_dead and order.offset == order_offset and order.direction == order_dir ]) else: order_offset = "CLOSE" frozen_volume = pending_frozen + sum([ order.volume_left for order in self._pos.orders.values() if not order.is_dead and order.offset != "OPEN" and order.direction == order_dir ]) pos_all = self._pos.pos_short_today if vol > 0 else self._pos.pos_long_today order_volume = min(abs(vol), max(0, pos_all - frozen_volume)) elif offset == "开": order_offset = "OPEN" order_volume = abs(vol) else: order_offset = "" order_volume = 0 return order_offset, order_dir, order_volume async def _update_time_from_md(self): """监听行情更新并记录当时本地时间的task""" async with self._api.register_update_notify( self._quote) as quote_update_chan: async for _ in quote_update_chan: # quote有更新时:更新记录的时间 self._local_time_record = time.time() - 0.005 # 更新最新行情时间时的本地时间 self._local_time_record_update_chan.send_nowait( True) # 通知记录的时间有更新 async def _target_pos_task(self): """负责调整目标持仓的task""" try: async for target_pos in self._pos_chan: # lib 中对于时间判断的方案: # 如果当前时间(模拟交易所时间)不在交易时间段内,则:等待直到行情更新 # 行情更新(即下一交易时段开始)后:获取target_pos最新的目标仓位, 开始调整仓位 # 如果不在可交易时间段内: 等待更新 while not _is_in_trading_time(self._quote, self._quote["datetime"], self._local_time_record): await self._local_time_record_update_chan.recv() target_pos = self._pos_chan.recv_latest( target_pos) # 获取最后一个target_pos目标仓位 # 确定调仓增减方向 delta_volume = target_pos - self._pos.pos pending_forzen = 0 all_tasks = [] for each_priority in self._offset_priority + ",": # 按不同模式的优先级顺序报出不同的offset单,股指(“昨开”)平昨优先从不平今就先报平昨,原油平今优先("今昨开")就报平今 if each_priority == ",": await gather(*[each._task for each in all_tasks]) pending_forzen = 0 all_tasks = [] continue order_offset, order_dir, order_volume = self._get_order( each_priority, delta_volume, pending_forzen) if order_volume == 0: # 如果没有则直接到下一种offset continue elif order_offset != "OPEN": pending_forzen += order_volume order_task = InsertOrderUntilAllTradedTask( self._api, self._symbol, order_dir, offset=order_offset, volume=order_volume, price=self._price, trade_chan=self._trade_chan) all_tasks.append(order_task) delta_volume -= order_volume if order_dir == "BUY" else -order_volume finally: # 执行 task.cancel() 时, 删除掉该 symbol 对应的 TargetPosTask 实例 TargetPosTaskSingleton._instances.pop(self._symbol, None)