class TqBacktest(object): """ 天勤回测类 该类只针对天勤外部IDE编写使用, 在天勤内编写完策略后选择该策略直接点击回测即可。 将该类传入 TqApi 的构造函数, 则策略就会进入回测模式。 回测模式下 k线会在刚创建出来时和结束时分别更新一次, 在这之间 k线是不会更新的。 回测模式下 quote 的更新频率由所订阅的 tick 和 k线周期确定: * 只要订阅了 tick, 则对应合约的 quote 就会使用 tick 生成, 更新频率也和 tick 一致, 但 **只有下字段** : 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, 但是订阅了 k线, 则对应合约的 quote 会使用 k线生成, 更新频率和 k线的周期一致, 如果订阅了某个合约的多个周期的 k线, 则任一个周期的 k线有更新时, quote 都会更新. 使用 k线生成的 quote 的盘口由收盘价分别加/减一个最小变动单位, 并且 highest/lowest/average/amount 始终为 nan, volume 始终为0 * 如果即没有订阅 tick, 也没有订阅k线或 订阅的k线周期大于分钟线, 则 TqBacktest 会 **自动订阅分钟线** 来生成 quote **注意** :如果未订阅 quote,模拟交易在下单时会自动为此合约订阅 quote ,根据回测时 quote 的更新规则,如果此合约没有订阅K线或K线周期大于分钟线 **则会自动订阅一个分钟线** 。 模拟交易要求报单价格大于等于对手盘价格才会成交, 例如下买单, 要求价格大于等于卖一价才会成交, 如果不能立即成交则会等到下次行情更新再重新判断。 回测模式下 wait_update 每次最多推进一个行情时间。 回测结束后会抛出 BacktestFinished 例外。 对 **组合合约** 进行回测时需注意:只能通过订阅 tick 数据来回测,不能订阅K线,因为K线是由最新价合成的,而交易所发回的组合合约数据中无最新价。 """ def __init__(self, start_dt, end_dt): """ 创建天勤回测类 Args: start_dt (date/datetime): 回测起始时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点 end_dt (date/datetime): 回测结束时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点 """ if isinstance(start_dt, datetime): self.current_dt = int(start_dt.timestamp() * 1e9) elif isinstance(start_dt, date): self.current_dt = TqApi._get_trading_day_start_time( int( datetime(start_dt.year, start_dt.month, start_dt.day).timestamp()) * 1000000000) else: raise Exception( "回测起始时间(start_dt)类型 %s 错误, 请检查 start_dt 数据类型是否填写正确" % (type(start_dt))) if isinstance(end_dt, datetime): self.end_dt = int(end_dt.timestamp() * 1e9) elif isinstance(end_dt, date): self.end_dt = TqApi._get_trading_day_end_time( int( datetime(end_dt.year, end_dt.month, end_dt.day).timestamp()) * 1000000000) else: raise Exception("回测结束时间(end_dt)类型 %s 错误, 请检查 end_dt 数据类型是否填写正确" % (type(end_dt))) async def _run(self, api, sim_send_chan, sim_recv_chan, md_send_chan, md_recv_chan): """回测task""" self.api = api self.logger = api._logger.getChild("TqBacktest") # 调试信息输出 self.sim_send_chan = sim_send_chan self.sim_recv_chan = sim_recv_chan self.md_send_chan = md_send_chan self.md_recv_chan = md_recv_chan self.pending_peek = False self.data = Entity() # 数据存储 self.data._instance_entity([]) self.serials = {} # 所有原始数据序列 self.quotes = {} self.diffs = [] md_task = self.api.create_task(self._md_handler()) try: await self._send_snapshot() async for pack in self.sim_send_chan: self.logger.debug("TqBacktest message received: %s", pack) if pack["aid"] == "subscribe_quote": self.diffs.append({"ins_list": pack["ins_list"]}) for ins in pack["ins_list"].split(","): await self._ensure_quote(ins) await self._send_diff() # 处理上一次未处理的 peek_message elif pack["aid"] == "set_chart": if pack["ins_list"]: # 回测模块中已保证每次将一个行情时间的数据全部发送给api,因此更新行情时 保持与初始化时一样的charts信息(即不作修改) self.diffs.append({ "charts": { pack["chart_id"]: { # 两个id设置为0:保证api在回测中判断此值时不是-1,即直接通过对数据接收完全的验证 "left_id": 0, "right_id": 0, "more_data": False, # 直接发送False给api,表明数据发送完全,使api中通过数据接收完全的验证 "state": pack } } }) await self._ensure_serial(pack["ins_list"], pack["duration"]) else: self.diffs.append({"charts": {pack["chart_id"]: None}}) await self._send_diff() # 处理上一次未处理的 peek_message elif pack["aid"] == "peek_message": self.pending_peek = True await self._send_diff() finally: # 关闭所有serials for s in self.serials.values(): await s["generator"].aclose() md_task.cancel() async def _md_handler(self): async for pack in self.md_recv_chan: await self.md_send_chan.send({"aid": "peek_message"}) for d in pack.get("data", []): TqApi._merge_diff(self.data, d, self.api._prototype, False) async def _send_snapshot(self): """发送初始合约信息""" async with TqChan(self.api, last_only=True) as update_chan: # 等待与行情服务器连接成功 self.data["_listener"].add(update_chan) while self.data.get("mdhis_more_data", True): await update_chan.recv() # 发送合约信息截面 quotes = {} for ins, quote in self.data["quotes"].items(): if not ins.startswith("_"): quotes[ins] = { "open": None, # 填写None: 删除api中的这个字段 "close": None, "settlement": None, "lower_limit": None, "upper_limit": None, "pre_open_interest": None, "pre_settlement": None, "pre_close": None, "ins_class": quote.get("ins_class", ""), "margin": quote.get( "margin"), # 用于内部实现模拟交易, 不作为api对外可用数据(即 Quote 类中无此字段) "commission": quote.get("commission" ), # 用于内部实现模拟交易, 不作为api对外可用数据(即 Quote 类中无此字段) "price_tick": quote["price_tick"], "price_decs": quote["price_decs"], "volume_multiple": quote["volume_multiple"], "max_limit_order_volume": quote["max_limit_order_volume"], "max_market_order_volume": quote["max_market_order_volume"], "min_limit_order_volume": quote["min_limit_order_volume"], "min_market_order_volume": quote["min_market_order_volume"], "underlying_symbol": quote["underlying_symbol"], "strike_price": quote["strike_price"], "expired": None, "trading_time": quote.get("trading_time"), "expire_datetime": quote.get("expire_datetime"), "delivery_month": quote.get("delivery_month"), "delivery_year": quote.get("delivery_year"), } self.diffs.append({ "quotes": quotes, "ins_list": "", "mdhis_more_data": False, }) async def _send_diff(self): """发送数据到 api, 如果 self.diffs 不为空则发送 self.diffs, 不推进行情时间, 否则将时间推进一格, 并发送对应的行情""" if self.pending_peek: quotes = {} if not self.diffs: while self.serials: min_serial = min( self.serials.keys(), key=lambda serial: self.serials[serial]["timestamp"]) timestamp = self.serials[min_serial][ "timestamp"] # 所有已订阅数据中的最小行情时间 quotes_diff = self.serials[min_serial]["quotes"] # 推进时间,一次只会推进最多一个(补数据时有可能是0个)行情时间,并确保<=该行情时间的行情都被发出 # 如果行情时间大于当前回测时间 则 判断是否diff中已有数据;否则表明此行情时间的数据未全部保存在diff中,则继续append if timestamp > self.current_dt: if self.diffs: # 如果diffs中已有数据:退出循环并发送数据给下游api break else: self.current_dt = timestamp # 否则将回测时间更新至最新行情时间 self.diffs.append(self.serials[min_serial]["diff"]) quote_info = self.quotes[min_serial[0]] if quotes_diff and (quote_info["min_duration"] != 0 or min_serial[1] == 0): quotes[min_serial[0]] = quotes_diff await self._fetch_serial(min_serial) if not self.serials: # 当无可发送数据时则抛出BacktestFinished例外,包括未订阅任何行情 或 所有已订阅行情的最后一笔行情获取完成 self.logger.warning("回测结束") raise BacktestFinished() from None for ins, diff in quotes.items(): for d in diff: self.diffs.append({"quotes": {ins: d}}) if self.diffs: rtn_data = { "aid": "rtn_data", "data": self.diffs, } self.diffs = [] self.pending_peek = False self.logger.debug("backtest message send: %s", rtn_data) await self.sim_recv_chan.send(rtn_data) async def _ensure_serial(self, ins, dur): if (ins, dur) not in self.serials: quote = self.quotes.setdefault( ins, { # 在此处设置 min_duration: 每次生成K线的时候会自动生成quote, 记录某一合约的最小duration "min_duration": dur }) quote["min_duration"] = min(quote["min_duration"], dur) self.serials[(ins, dur)] = { "generator": self._gen_serial(ins, dur), } await self._fetch_serial((ins, dur)) async def _ensure_quote(self, ins): if ins not in self.quotes or self.quotes[ins][ "min_duration"] > 60000000000: await self._ensure_serial(ins, 60000000000) async def _fetch_serial(self, serial): s = self.serials[serial] try: s["timestamp"], s["diff"], s["quotes"] = await s["generator" ].__anext__() except StopAsyncIteration: del self.serials[serial] # 删除一个行情时间超过结束时间的serial async def _gen_serial(self, ins, dur): """k线/tick 序列的 async generator, yield 出来的行情数据带有时间戳, 因此 _send_diff 可以据此归并""" # 先定位左端点, focus_datetime 是 lower_bound ,这里需要的是 upper_bound # 因此将 view_width 和 focus_position 设置成一样,这样 focus_datetime 所对应的 k线刚好位于屏幕外 chart_info = { "aid": "set_chart", "chart_id": TqApi._generate_chart_id("backtest"), "ins_list": ins, "duration": dur, "view_width": 8964, # 设为8964原因:可满足用户所有的订阅长度,并在backtest中将所有的 相同合约及周期 的K线用同一个serial存储 "focus_datetime": int(self.current_dt), "focus_position": 8964, } chart = TqApi._get_obj(self.data, ["charts", chart_info["chart_id"]]) current_id = None # 当前数据指针 serial = TqApi._get_obj( self.data, ["klines", ins, str(dur)] if dur != 0 else ["ticks", ins]) async with TqChan(self.api, last_only=True) as update_chan: serial["_listener"].add(update_chan) chart["_listener"].add(update_chan) await self.md_send_chan.send(chart_info.copy()) try: async for _ in update_chan: if not (chart_info.items() <= TqApi._get_obj( chart, ["state"]).items()): # 当前请求还没收齐回应, 不应继续处理 continue left_id = chart.get("left_id", -1) right_id = chart.get("right_id", -1) last_id = serial.get("last_id", -1) if (left_id == -1 and right_id == -1) or last_id == -1: # 定位信息还没收到, 或数据序列还没收到 continue if self.data.get("mdhis_more_data", True): self.data["_listener"].add(update_chan) continue else: self.data["_listener"].discard(update_chan) if current_id is None: current_id = max(left_id, 0) while True: if current_id > last_id: # 当前 id 已超过 last_id return if current_id - chart_info.get("left_kline_id", left_id) > 5000: # 当前 id 已超出订阅范围, 需重新订阅后续数据 chart_info["left_kline_id"] = current_id chart_info.pop("focus_datetime", None) chart_info.pop("focus_position", None) await self.md_send_chan.send(chart_info.copy()) # 将订阅的8964长度的窗口中的数据都遍历完后,退出循环,然后再次进入并处理下一窗口数据 # (因为在处理过5000条数据的同时向服务器订阅从当前id开始的新一窗口的数据,在当前窗口剩下的3000条数据处理完后,下一窗口数据也已经收到) if current_id > right_id: break item = { k: v for k, v in serial["data"].get( str(current_id), {}).items() } if dur == 0: diff = { "ticks": { ins: { "last_id": current_id, "data": { str(current_id): item, str(current_id - 8964): None, } } } } if item["datetime"] > self.end_dt: # 超过结束时间 return yield item[ "datetime"], diff, self._get_quotes_from_tick( item) else: diff = { "klines": { ins: { str(dur): { "last_id": current_id, "data": { str(current_id): { "datetime": item["datetime"], "open": item["open"], "high": item["open"], "low": item["open"], "close": item["open"], "volume": 0, "open_oi": item["open_oi"], "close_oi": item["open_oi"], }, str(current_id - 8964): None, } } } } } timestamp = item[ "datetime"] if dur < 86400000000000 else TqApi._get_trading_day_start_time( item["datetime"]) if timestamp > self.end_dt: # 超过结束时间 return yield timestamp, diff, None # K线刚生成时的数据都为开盘价 diff = { "klines": { ins: { str(dur): { "data": { str(current_id): item, } } } } } timestamp = item[ "datetime"] + dur - 1000 if dur < 86400000000000 else TqApi._get_trading_day_end_time( item["datetime"]) if timestamp > self.end_dt: # 超过结束时间 return yield timestamp, diff, self._get_quotes_from_kline( self.data["quotes"][ins], timestamp, item) # K线结束时生成quote数据 current_id += 1 finally: # 释放chart资源 chart_info["ins_list"] = "" await self.md_send_chan.send(chart_info.copy()) @staticmethod def _get_quotes_from_tick(tick): quote = {k: v for k, v in tick.items()} quote["datetime"] = datetime.fromtimestamp( tick["datetime"] / 1e9).strftime("%Y-%m-%d %H:%M:%S.%f") return [quote] @staticmethod def _get_quotes_from_kline(info, timestamp, kline): return [{ "datetime": datetime.fromtimestamp(timestamp / 1e9).strftime("%Y-%m-%d %H:%M:%S.%f"), "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"], }]
class TqBacktest(object): """ 天勤回测类 该类只针对天勤外部IDE编写使用, 在天勤内编写完策略后选择该策略直接点击回测即可 将该类传入 TqApi 的构造函数, 则策略就会进入回测模式 回测模式下 k线会在刚创建出来时和结束时分别更新一次, 在这之间 k线是不会更新的 回测模式下 quote 的更新频率由所订阅的 tick 和 k线周期确定: * 只要订阅了 tick, 则对应合约的 quote 就会使用 tick 生成, 更新频率也和 tick 一致, 但只有下字段: 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, 但是订阅了 k线, 则对应合约的 quote 会使用 k线生成, 更新频率和 k线的周期一致, 如果订阅了某个合约的多个周期的 k线, 则任一个周期的 k线有更新时, quote 都会更新. 使用 k线生成的 quote 的盘口由收盘价分别加/减一个最小变动单位, 并且 highest/lowest/average/amount 始终为 nan, volume 始终为0 * 如果即没有订阅 tick, 也没有订阅 k线或订阅的 k线周期大于分钟线, 则 TqBacktest 会自动订阅分钟线来生成 quote 模拟交易要求报单价格大于等于对手盘价格才会成交, 例如下买单, 要求价格大于等于卖一价才会成交, 如果不能立即成交则会等到下次行情更新再重新判断 回测模式下 wait_update 每次最多推进一个行情时间 回测结束后会抛出 BacktestFinished 例外 """ def __init__(self, start_dt, end_dt): """ 创建天勤回测类 Args: start_dt (date/datetime): 回测起始时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点 end_dt (date/datetime): 回测结束时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点 """ if isinstance(start_dt, datetime): self.current_dt = int(start_dt.timestamp()*1e9) elif isinstance(start_dt, date): self.current_dt = TqApi._get_trading_day_start_time(int(datetime(start_dt.year, start_dt.month, start_dt.day).timestamp())*1000000000) else: raise Exception("回测起始时间(start_dt)类型 %s 错误, 请检查 start_dt 数据类型是否填写正确" % (type(start_dt))) if isinstance(end_dt, datetime): self.end_dt = int(end_dt.timestamp()*1e9) elif isinstance(end_dt, date): self.end_dt = TqApi._get_trading_day_end_time(int(datetime(end_dt.year, end_dt.month, end_dt.day).timestamp())*1000000000) else: raise Exception("回测结束时间(end_dt)类型 %s 错误, 请检查 end_dt 数据类型是否填写正确" % (type(end_dt))) async def _run(self, api, sim_send_chan, sim_recv_chan, md_send_chan, md_recv_chan): """回测task""" self.api = api self.logger = api.logger.getChild("TqBacktest") # 调试信息输出 self.sim_send_chan = sim_send_chan self.sim_recv_chan = sim_recv_chan self.md_send_chan = md_send_chan self.md_recv_chan = md_recv_chan self.pending_peek = False self.data = Entity() # 数据存储 self.data["_path"] = [] self.data["_listener"] = weakref.WeakSet() self.serials = {} # 所有原始数据序列 self.quotes = {} self.diffs = [] md_task = self.api.create_task(self._md_handler()) try: await self._send_snapshot() async for pack in self.sim_send_chan: self.logger.debug("TqBacktest message received: %s", pack) if pack["aid"] == "subscribe_quote": self.diffs.append({"ins_list": pack["ins_list"]}) for ins in pack["ins_list"].split(","): await self._ensure_quote(ins) await self._send_diff() elif pack["aid"] == "set_chart": if pack["ins_list"]: self.diffs.append({"charts": {pack["chart_id"]: {"state": pack}}}) await self._ensure_serial(pack["ins_list"], pack["duration"]) else: self.diffs.append({"charts": {pack["chart_id"]: None}}) await self._send_diff() elif pack["aid"] == "peek_message": self.pending_peek = True await self._send_diff() finally: # 关闭所有serials for s in self.serials.values(): await s["generator"].aclose() md_task.cancel() async def _md_handler(self): async for pack in self.md_recv_chan: await self.md_send_chan.send({"aid": "peek_message"}) for d in pack.get("data", []): TqApi._merge_diff(self.data, d, self.api.prototype, False) async def _send_snapshot(self): """发送初始合约信息""" async with TqChan(self.api, last_only=True) as update_chan: self.data["_listener"].add(update_chan) while self.data.get("mdhis_more_data", True): await update_chan.recv() # 发送合约信息截面 quotes = {} for ins, quote in self.data["quotes"].items(): if not ins.startswith("_"): quotes[ins] = { "open": None, "close": None, "settlement": None, "lower_limit": None, "upper_limit": None, "pre_open_interest": None, "pre_settlement": None, "pre_close": None, "margin": quote.get("margin"), "commission": quote.get("commission"), "price_tick": quote["price_tick"], "price_decs": quote["price_decs"], "volume_multiple": quote["volume_multiple"], "max_limit_order_volume": quote["max_limit_order_volume"], "max_market_order_volume": quote["max_market_order_volume"], "min_limit_order_volume": quote["min_limit_order_volume"], "min_market_order_volume": quote["min_market_order_volume"], "underlying_symbol": quote["underlying_symbol"], "strike_price": quote["strike_price"], "change": None, "change_percent": None, "expired": None, } self.diffs.append({ "quotes": quotes, "ins_list": "", "mdhis_more_data": False, }) async def _send_diff(self): """发送数据到 api, 如果 self.diffs 不为空则发送 self.diffs, 不推进行情时间, 否则将时间推进一格, 并发送对应的行情""" if self.pending_peek: quotes = {} if not self.diffs: while self.serials: min_serial = min(self.serials.keys(), key=lambda serial: self.serials[serial]["timestamp"]) timestamp = self.serials[min_serial]["timestamp"] quotes_diff = self.serials[min_serial]["quotes"] # 推进时间,一次只会推进最多一个(补数据时有可能是0个)行情时间,并确保<=该行情时间的行情都被发出 if timestamp > self.current_dt: if self.diffs: break else: self.current_dt = timestamp self.diffs.append(self.serials[min_serial]["diff"]) quote_info = self.quotes[min_serial[0]] if quotes_diff and (quote_info["min_duration"] != 0 or min_serial[1] == 0): quotes[min_serial[0]] = quotes_diff await self._fetch_serial(min_serial) for ins, diff in quotes.items(): for d in diff: self.diffs.append({"quotes": {ins: d}}) if self.diffs: rtn_data = { "aid": "rtn_data", "data": self.diffs, } self.diffs = [] self.pending_peek = False self.logger.debug("backtest message send: %s", rtn_data) await self.sim_recv_chan.send(rtn_data) async def _ensure_serial(self, ins, dur): if (ins, dur) not in self.serials: quote = self.quotes.setdefault(ins, {"min_duration": dur}) quote["min_duration"] = min(quote["min_duration"], dur) self.serials[(ins, dur)] = { "generator": self._gen_serial(ins, dur), } await self._fetch_serial((ins, dur)) async def _ensure_quote(self, ins): if ins not in self.quotes or self.quotes[ins]["min_duration"] > 60000000000: await self._ensure_serial(ins, 60000000000) async def _fetch_serial(self, serial): s = self.serials[serial] try: s["timestamp"], s["diff"], s["quotes"] = await s["generator"].__anext__() except StopAsyncIteration: del self.serials[serial] if not self.serials: raise BacktestFinished() from None async def _gen_serial(self, ins, dur): """k线/tick 序列的 async generator, yield 出来的行情数据带有时间戳, 因此 _send_diff 可以据此归并""" # 先定位左端点, focus_datetime 是 lower_bound ,这里需要的是 upper_bound # 因此将 view_width 和 focus_position 设置成一样,这样 focus_datetime 所对应的 k线刚好位于屏幕外 chart_info = { "aid": "set_chart", "chart_id": TqApi._generate_chart_id("backtest", ins, dur//1000000000), "ins_list": ins, "duration": dur, "view_width": 8964, "focus_datetime": int(self.current_dt), "focus_position": 8964, } chart = TqApi._get_obj(self.data, ["charts", chart_info["chart_id"]]) current_id = None # 当前数据指针 serial = TqApi._get_obj(self.data, ["klines", ins, str(dur)] if dur != 0 else ["ticks", ins]) async with TqChan(self.api, last_only=True) as update_chan: serial["_listener"].add(update_chan) chart["_listener"].add(update_chan) await self.md_send_chan.send(chart_info.copy()) try: async for _ in update_chan: if not (chart_info.items() <= TqApi._get_obj(chart, ["state"]).items()): # 当前请求还没收齐回应, 不应继续处理 continue left_id = chart.get("left_id", -1) right_id = chart.get("right_id", -1) last_id = serial.get("last_id", -1) if (left_id == -1 and right_id == -1) or last_id == -1: # 定位信息还没收到, 或数据序列还没收到 continue if self.data.get("mdhis_more_data", True): self.data["_listener"].add(update_chan) continue else: self.data["_listener"].discard(update_chan) if current_id is None: current_id = max(left_id, 0) while True: if current_id > last_id: # 当前 id 已超过 last_id return if current_id - chart_info.get("left_kline_id", left_id) > 5000: # 当前 id 已超出订阅范围, 需重新订阅后续数据 chart_info["left_kline_id"] = current_id chart_info.pop("focus_datetime", None) chart_info.pop("focus_position", None) await self.md_send_chan.send(chart_info.copy()) if current_id > right_id: break item = serial["data"].get(str(current_id), {}).copy() del item["_path"] del item["_listener"] if dur == 0: diff = { "ticks": { ins: { "last_id": current_id, "data": { str(current_id): item, str(current_id-8964): None, } } } } if item["datetime"] > self.end_dt: # 超过结束时间 return yield item["datetime"], diff, self._get_quotes_from_tick(item) else: diff = { "klines": { ins: { str(dur): { "last_id": current_id, "data": { str(current_id): { "datetime": item["datetime"], "open": item["open"], "high": item["open"], "low": item["open"], "close": item["open"], "volume": 0, "open_oi": item["open_oi"], "close_oi": item["open_oi"], }, str(current_id-8964): None, } } } } } timestamp = item["datetime"] if dur < 86400000000000 else TqApi._get_trading_day_start_time(item["datetime"]) if timestamp > self.end_dt: # 超过结束时间 return yield timestamp, diff, None diff = { "klines": { ins: { str(dur): { "data": { str(current_id): item, } } } } } timestamp = item["datetime"] + dur - 1000 if dur < 86400000000000 else TqApi._get_trading_day_end_time(item["datetime"]) if timestamp > self.end_dt: # 超过结束时间 return yield timestamp, diff, self._get_quotes_from_kline(self.data["quotes"][ins], timestamp, item) current_id += 1 finally: # 释放chart资源 chart_info["ins_list"] = "" await self.md_send_chan.send(chart_info.copy()) @staticmethod def _get_quotes_from_tick(tick): quote = tick.copy() quote["datetime"] = datetime.fromtimestamp(tick["datetime"] / 1e9).strftime("%Y-%m-%d %H:%M:%S.%f") return [quote] @staticmethod def _get_quotes_from_kline(info, timestamp, kline): return [ { "datetime": datetime.fromtimestamp(timestamp / 1e9).strftime("%Y-%m-%d %H:%M:%S.%f"), "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"], } ]