def _md_recv(self, pack): for d in pack["data"]: self._diffs.append(d) # 在第一次收到 mdhis_more_data 为 False 的时候,发送账户初始截面信息,这样回测模式下,往后的模块才有正确的时间顺序 if not self._has_send_init_account and not d.get( "mdhis_more_data", True): self._diffs.append(self._sim_trade.init_snapshot()) self._diffs.append( {"trade": { self._account_key: { "trade_more_data": False } }}) self._has_send_init_account = True _tqsdk_backtest = d.get("_tqsdk_backtest", {}) if _tqsdk_backtest: # 回测时,用 _tqsdk_backtest 对象中 current_dt 作为 TqSim 的 _current_datetime self._tqsdk_backtest.update(_tqsdk_backtest) self._current_datetime = _timestamp_nano_to_str( self._tqsdk_backtest["current_dt"]) self._local_time_record = float("nan") # 1. 回测时不使用时间差来模拟交易所时间的原因(_local_time_record始终为初始值nan): # 在sim收到行情后记录_local_time_record,然后下发行情到api进行merge_diff(),api需要处理完k线和quote才能结束wait_update(), # 若处理时间过长,此时下单则在判断下单时间时与测试用例中的预期时间相差较大,导致测试用例无法通过。 # 2. 回测不使用时间差的方法来判断下单时间仍是可行的: 与使用了时间差的方法相比, 只对在每个交易时间段最后一笔行情时的下单时间判断有差异, # 若不使用时间差, 则在最后一笔行情时下单仍判断为在可交易时间段内, 且可成交. quotes_diff = d.get("quotes", {}) # 先根据 quotes_diff 里的 datetime, 确定出 _current_datetime,再 _merge_diff(同时会发送行情到 quote_chan) for symbol, quote_diff in quotes_diff.items(): if quote_diff is None: continue # 若直接使用本地时间来判断下单时间是否在可交易时间段内 可能有较大误差,因此判断的方案为:(在接收到下单指令时判断 估计的交易所时间 是否在交易时间段内) # 在更新最新行情时间(即self._current_datetime)时,记录当前本地时间(self._local_time_record), # 在这之后若收到下单指令,则获取当前本地时间,判 "最新行情时间 + (当前本地时间 - 记录的本地时间)" 是否在交易时间段内。 # 另外, 若在盘后下单且下单前未订阅此合约: # 因为从_md_recv()中获取数据后立即判断下单时间则速度过快(两次time.time()的时间差小于最后一笔行情(14:59:9995)到15点的时间差), # 则会立即成交,为处理此情况则将当前时间减去5毫秒(模拟发生5毫秒网络延迟,则两次time.time()的时间差增加了5毫秒)。 # todo: 按交易所来存储 _current_datetime(issue: #277) if quote_diff.get("datetime", "") > self._current_datetime: # 回测时,当前时间更新即可以由 quote 行情更新,也可以由 _tqsdk_backtest.current_dt 更新, # 在最外层的循环里,_tqsdk_backtest.current_dt 是在 rtn_data.data 中数组位置中的最后一个,会在循环最后一个才更新 self.current_datetime # 导致前面处理 order 时的 _current_datetime 还是旧的行情时间 self._current_datetime = quote_diff["datetime"] # 最新行情时间 # 更新最新行情时间时的本地时间,回测时不使用时间差 self._local_time_record = ( time.time() - 0.005) if not self._tqsdk_backtest else float("nan") if self._current_datetime > self._trading_day_end: # 结算 self._settle() # 若当前行情时间大于交易日的结束时间(切换交易日),则根据此行情时间更新交易日及交易日结束时间 trading_day = _get_trading_day_from_timestamp( self._get_current_timestamp()) self._trading_day_end = _timestamp_nano_to_str( _get_trading_day_end_time(trading_day) - 999) if quotes_diff: _merge_diff(self._data, {"quotes": quotes_diff}, self._prototype, persist=False, reduce_diff=False, notify_update_diff=True)
def _get_quotes_from_kline(info, timestamp, kline): """ 分为三个包发给下游: 1. 根据 diff 协议,对于用户收到的最终结果没有影响 2. TqSim 撮合交易会按顺序处理收到的包,分别比较 high、low、close 三个价格对应的买卖价 3. TqSim 撮合交易只用到了买卖价,所以最新价只产生一次 close,而不会发送三次 """ return [ { "datetime": _timestamp_nano_to_str(timestamp), "ask_price1": kline["high"] + info["price_tick"], "ask_volume1": 1, "bid_price1": kline["high"] - info["price_tick"], "bid_volume1": 1, "last_price": kline["close"], "highest": float("nan"), "lowest": float("nan"), "average": float("nan"), "volume": 0, "amount": float("nan"), "open_interest": kline["close_oi"], }, { "ask_price1": kline["low"] + info["price_tick"], "bid_price1": kline["low"] - info["price_tick"], }, { "ask_price1": kline["close"] + info["price_tick"], "bid_price1": kline["close"] - info["price_tick"], } ]
def _get_quotes_from_kline_open(info, timestamp, kline): return [ { # K线刚生成时的数据都为开盘价 "datetime": _timestamp_nano_to_str(timestamp), "ask_price1": kline["open"] + info["price_tick"], "ask_volume1": 1, "bid_price1": kline["open"] - info["price_tick"], "bid_volume1": 1, "last_price": kline["open"], "highest": float("nan"), "lowest": float("nan"), "average": float("nan"), "volume": 0, "amount": float("nan"), "open_interest": kline["open_oi"], }, ]
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 _get_quotes_from_tick(tick): quote = {k: v for k, v in tick.items()} quote["datetime"] = _timestamp_nano_to_str(tick["datetime"]) return [quote]