def __init__(self, start_dt: Union[date, datetime], end_dt: Union[date, datetime]) -> None: """ 创建天勤回测类 Args: start_dt (date/datetime): 回测起始时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点 end_dt (date/datetime): 回测结束时间, 如果类型为 date 则指的是交易日, 如果为 datetime 则指的是具体时间点 """ if isinstance(start_dt, datetime): self._start_dt = int(start_dt.timestamp() * 1e9) elif isinstance(start_dt, date): self._start_dt = _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 = _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))) self._current_dt = self._start_dt # 记录当前的交易日 开始时间/结束时间 self._trading_day_start = _get_trading_day_start_time(_get_trading_day_from_timestamp(self._current_dt)) self._trading_day_end = _get_trading_day_end_time(_get_trading_day_from_timestamp(self._current_dt))
async def _send_diff(self): """发送数据到 api, 如果 self._diffs 不为空则发送 self._diffs, 不推进行情时间, 否则将时间推进一格, 并发送对应的行情""" if self._pending_peek: if not self._diffs: quotes = await self._generator_diffs(False) else: quotes = await self._generator_diffs(True) for ins, diff in quotes.items(): self._quotes[ins]["sended_init_quote"] = True for d in diff: self._diffs.append({ "quotes": { ins: d } }) if self._diffs: # 发送数据集中添加 backtest 字段,开始时间、结束时间、当前时间,表示当前行情推进是由 backtest 推进 if self._is_first_send: self._diffs.append({ "_tqsdk_backtest": { "start_dt": self._start_dt, "current_dt": self._current_dt, "end_dt": self._end_dt } }) self._is_first_send = False else: self._diffs.append({ "_tqsdk_backtest": { "current_dt": self._current_dt } }) # 切换交易日,将历史的主连合约信息添加的 diffs if self._current_dt > self._trading_day_end: # 使用交易日结束时间,每个交易日切换只需要计算一次交易日结束时间 # 相比发送 diffs 前每次都用 _current_dt 计算当前交易日,计算次数更少 self._trading_day_start = _get_trading_day_start_time(_get_trading_day_from_timestamp(self._current_dt)) self._trading_day_end = _get_trading_day_end_time(_get_trading_day_from_timestamp(self._current_dt)) self._diffs.append({ "quotes": self._get_history_cont_quotes( datetime.fromtimestamp(self._trading_day_end / 1e9).strftime("%Y%m%d") ) }) self._diffs.append({ "quotes": {k: {'expired': v.get('expire_datetime', float('nan')) <= self._trading_day_start} for k, v in self._data.get('quotes').items()} }) self._sim_recv_chan_send_count += 1 if self._sim_recv_chan_send_count > 10000: self._sim_recv_chan_send_count = 0 self._diffs.append(self._gc_data()) rtn_data = { "aid": "rtn_data", "data": self._diffs, } self._diffs = [] self._pending_peek = False await self._sim_recv_chan.send(rtn_data)
def _get_trading_timestamp(quote, current_datetime: str): """ 将 quote 在 current_datetime 所在交易日的所有可交易时间段转换为纳秒时间戳(tqsdk内部使用的时间戳统一为纳秒)并返回 """ # 获取当前交易日时间戳 current_trading_day_timestamp = _get_trading_day_from_timestamp( int(datetime.datetime.strptime(current_datetime, "%Y-%m-%d %H:%M:%S.%f").timestamp() * 1e6) * 1000) # 获取上一交易日时间戳 last_trading_day_timestamp = _get_trading_day_from_timestamp( _get_trading_day_start_time(current_trading_day_timestamp) - 1) trading_timestamp = { "day": TqSim._get_period_timestamp(current_trading_day_timestamp, quote["trading_time"].get("day", [])), "night": TqSim._get_period_timestamp(last_trading_day_timestamp, quote["trading_time"].get("night", [])) } return trading_timestamp
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 _on_recv_data(self, diffs): for d in diffs: self._datetime_state.update_state(d) current = self._datetime_state.get_current_dt() if current > self._trading_day_end: # 切换交易日 self._trading_day_end = _get_trading_day_end_time( _get_trading_day_from_timestamp(current)) [r._on_settle() for r in self]
def _get_dividend_ratio(quote): # 获取合约下一个交易日的送股、分红信息 timestamp = _get_trading_day_from_timestamp( _get_trade_timestamp(quote['datetime'], float('nan')) + 86400000000000) # 下一交易日 stock_dividend = _get_dividend_ratio_by_dt(quote['stock_dividend_ratio'], timestamp=timestamp) cash_dividend = _get_dividend_ratio_by_dt(quote['cash_dividend_ratio'], timestamp=timestamp) return stock_dividend, cash_dividend
def _generate_pend_diff(self): """" 补充期权额外字段 此函数在 send_diff() 才会调用, self._datetime_state.data_ready 一定为 True, 调用 self._datetime_state.get_current_dt() 一定有正确的当前时间 """ pend_diff = {} account_keys = list(self._data.get('trade', {}).keys()) objs_keys = ['positions', 'trades', 'orders'] # 如果有新添加的合约, 只填充一次即可 if self._new_objs_list: pend_diff.setdefault('trade', {k: {o_k: {} for o_k in objs_keys} for k in account_keys}) for obj in self._new_objs_list: # 新添加的 Position / Order / Trade 节点 if hasattr(obj, '_path') and obj['_path'][2] in objs_keys: account_key = obj['_path'][1] obj_key = obj['_path'][2] item_id = obj['_path'][3] quote = self._data_quotes.get(f"{obj.get('exchange_id', '')}.{obj.get('instrument_id', '')}", {}) if quote.get('ins_class', '').endswith('OPTION'): pend_diff_item = pend_diff['trade'][account_key][obj_key].setdefault(item_id, {}) pend_diff_item['option_class'] = quote.get('option_class') pend_diff_item['strike_price'] = quote.get('strike_price') pend_diff_item['underlying_symbol'] = quote.get('underlying_symbol') if quote.get('expire_datetime'): pend_diff_item['expire_rest_days'] = _get_expire_rest_days(quote.get('expire_datetime'), self._datetime_state.get_current_dt() / 1e9) self._new_objs_list.clear() # 如果有切换交易日,所有合约都需要修改 expire_rest_days current_dt = self._datetime_state.get_current_dt() if self._trading_day_end is None or current_dt > self._trading_day_end: pend_diff.setdefault('trade', {k: {o_k: {} for o_k in objs_keys} for k in account_keys}) for account_key, account_node in self._data.get('trade', {}).items(): for k in objs_keys: for item_id, item in account_node.get(k, {}).items(): quote = self._data_quotes.get(f"{item['exchange_id']}.{item['instrument_id']}", {}) if quote.get('ins_class', '').endswith('OPTION') and quote.get('expire_datetime'): pend_diff_item = pend_diff['trade'][account_key][k].setdefault(item_id, {}) # 剩余到期日字段,每天都会更新,每次都重新计算 pend_diff_item['expire_rest_days'] = _get_expire_rest_days(quote.get('expire_datetime'), current_dt / 1e9) self._trading_day_end = _get_trading_day_end_time(_get_trading_day_from_timestamp(current_dt)) return pend_diff
def _md_recv(self, pack): for d in pack["data"]: d.pop("trade", None) self._diffs.append(d) # 在第一次收到 mdhis_more_data 为 False 的时候,发送账户初始截面信息,这样回测模式下,往后的模块才有正确的时间顺序 if not self._has_send_init_account and not d.get("mdhis_more_data", True): self._send_account() self._diffs.append({ "trade": { self._account_id: { "orders": {}, "positions": {}, "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 = datetime.datetime.fromtimestamp( self._tqsdk_backtest["current_dt"] / 1e9).strftime("%Y-%m-%d %H:%M:%S.%f") self._local_time_record = float("nan") # 1. 回测时不使用时间差来模拟交易所时间的原因(_local_time_record始终为初始值nan): # 在sim收到行情后记录_local_time_record,然后下发行情到api进行merge_diff(),api需要处理完k线和quote才能结束wait_update(), # 若处理时间过长,此时下单则在判断下单时间时与测试用例中的预期时间相差较大,导致测试用例无法通过。 # 2. 回测不使用时间差的方法来判断下单时间仍是可行的: 与使用了时间差的方法相比, 只对在每个交易时间段最后一笔行情时的下单时间判断有差异, # 若不使用时间差, 则在最后一笔行情时下单仍判断为在可交易时间段内, 且可成交. for symbol, quote_diff in d.get("quotes", {}).items(): if quote_diff is None: continue quote = self._ensure_quote(symbol) quote["datetime"] = quote_diff.get("datetime", quote["datetime"]) # 若直接使用本地时间来判断下单时间是否在可交易时间段内 可能有较大误差,因此判断的方案为:(在接收到下单指令时判断 估计的交易所时间 是否在交易时间段内) # 在更新最新行情时间(即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["datetime"] > self._current_datetime and not self._tqsdk_backtest: self._current_datetime = quote["datetime"] # 最新行情时间 self._local_time_record = time.time() - 0.005 # 更新最新行情时间时的本地时间 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 = datetime.datetime.fromtimestamp( (_get_trading_day_end_time(trading_day) - 999) / 1e9).strftime("%Y-%m-%d %H:%M:%S.%f") if "ask_price1" in quote_diff: quote["ask_price1"] = float("nan") if type(quote_diff["ask_price1"]) is str else quote_diff[ "ask_price1"] if "bid_price1" in quote_diff: quote["bid_price1"] = float("nan") if type(quote_diff["bid_price1"]) is str else quote_diff[ "bid_price1"] if "last_price" in quote_diff: quote["last_price"] = float("nan") if type(quote_diff["last_price"]) is str else quote_diff[ "last_price"] quote["volume_multiple"] = quote_diff.get("volume_multiple", quote["volume_multiple"]) quote["commission"] = quote_diff.get("commission", quote["commission"]) quote["margin"] = quote_diff.get("margin", quote["margin"]) quote["trading_time"] = quote_diff.get("trading_time", quote["trading_time"]) self._match_orders(quote) if symbol in self._positions: self._adjust_position(symbol, price=quote["last_price"])
async def _run(self, api, sim_send_chan, sim_recv_chan, md_send_chan, md_recv_chan): """回测task""" self._api = api # 下载历史主连合约信息 start_trading_day = _get_trading_day_from_timestamp(self._start_dt) # 回测开始交易日 end_trading_day = _get_trading_day_from_timestamp(self._end_dt) # 回测结束交易日 self._continuous_table = TqBacktestContinuous(start_dt=start_trading_day, end_dt=end_trading_day, headers=self._api._base_headers) self._stock_dividend = TqBacktestDividend(start_dt=start_trading_day, end_dt=end_trading_day, headers=self._api._base_headers) 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._prototype = { "quotes": { "#": BtQuote(self._api), # 行情的数据原型 }, "klines": { "*": { "*": { "data": { "@": Kline(self._api), # K线的数据原型 } } } }, "ticks": { "*": { "data": { "@": Tick(self._api), # Tick的数据原型 } } } } self._sended_to_api = {} # 已经发给 api 的 rangeset (symbol, dur),只记录了 kline self._serials = {} # 所有用户请求的 chart 序列,如果用户订阅行情,默认请求 1 分钟 Kline # gc 是会循环 self._serials,来计算用户需要的数据,self._serials 不应该被删除, self._generators = {} # 所有用户请求的 chart 序列相应的 generator 对象,创建时与 self._serials 一一对应,会在一个序列计算到最后一根 kline 时被删除 self._had_any_generator = False # 回测过程中是否有过 generator 对象 self._sim_recv_chan_send_count = 0 # 统计向下游发送的 diff 的次数,每 1w 次执行一次 gc self._quotes = {} # 记录 min_duration 记录某一合约的最小duration; sended_init_quote 是否已经过这个合约的初始行情 self._diffs: List[Dict[str, Any]] = [] self._is_first_send = True md_task = self._api.create_task(self._md_handler()) try: await self._send_snapshot() async for pack in self._sim_send_chan: if pack["aid"] == "ins_query": await self._md_send_chan.send(pack) # 回测 query 不为空时需要ensure_query # 1. 在api初始化时会发送初始化请求(2.5.0版本开始已经不再发送初始化请求),接着会发送peek_message,如果这里没有等到结果,那么在收到 peek_message 的时候,会发现没有数据需要发送,回测结束 # 2. api在发送请求后,会调用 wait_update 更新数据,如果这里没有等到结果,行情可能会被推进 # query 为空时,表示清空数据的请求,这个可以直接发出去,不需要等到收到回复 if pack["query"] != "": await self._ensure_query(pack) await self._send_diff() elif pack["aid"] == "subscribe_quote": # todo: 回测时,用户如果先订阅日线,再订阅行情,会直接返回以日线 datetime 标识的行情信息,而不是当前真正的行情时间 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"], pack["chart_id"]) 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: # 关闭所有 generator for s in self._generators.values(): await s.aclose() md_task.cancel() await asyncio.gather(md_task, return_exceptions=True)
def vwap_table(api: TqApi, symbol: str, target_pos: int, duration: float, account: Optional[Union[TqAccount, TqKq, TqSim]] = None): """ 返回基于 vwap 策略的计划任务时间表。下单需要配合 TargetPosScheduler 使用。 调用 vwap_table 函数,根据以下逻辑生成 time_table: 1. 根据 target_pos - 当前合约的净持仓,得到总的需要调整手数 2. 请求 symbol 合约的 ``1min`` K 线 3. 采样取用最近 10 日内,以合约当前行情时间的下一分钟为起点,每日 duration / 60 根 K 线, \ 例如当前合约时间为 14:35:35,那么采样是会使用 14:36:00 开始的分钟线 K 线 4. 按日期分组,分别计算交易日内,每根 K 线成交量占总成交量的比例 5. 计算最近 10 日内相同分钟内的成交量占比的算术平均数,将第 1 步得到的总调整手数按照得到的比例分配 6. 每一分钟,前 58s 以追加价格下单,后 2s 以对价价格下单 Args: api (TqApi): TqApi实例,该task依托于指定api下单/撤单 symbol (str): 拟下单的合约 symbol, 格式为 交易所代码.合约代码, 例如 "SHFE.cu2201" target_pos (int): 目标持仓手数 duration (int): 算法执行的时长,以秒为单位,必须是 60 的整数倍,时长可以跨非交易时间段,但是不可以跨交易日 * 设置为 60*10, 可以是 10:10~10:15 + 10:30~10:35 account (TqAccount/TqKq/TqSim): [可选]指定发送下单指令的账户实例, 多账户模式下,该参数必须指定 Returns: pandas.DataFrame: 本函数返回一个 pandas.DataFrame 实例. 表示一份计划任务时间表。每一行表示一项目标持仓任务,包含以下列: + interval: 当前这项任务的持续时间长度,单位为秒 + target_pos: 当前这项任务的目标持仓 + price: 当前这项任务的下单价格模式,支持 PASSIVE(排队价),ACTIVE(对价),None(不下单,表示暂停一段时间) Example1:: from tqsdk import TqApi, TargetPosScheduler from tqsdk.algorithm import vwap_table api = TqApi(auth="信易账户,用户密码") quote = api.get_quote("CZCE.MA109") # 设置 vwap 任务参数 time_table = vwap_table(api, "CZCE.MA109", -100, 600) # 目标持仓 -100 手,600s 内完成 print(time_table.to_string()) target_pos_sch = TargetPosScheduler(api, "CZCE.MA109", time_table) # 启动循环 while not target_pos_sch.is_finished(): api.wait_update() api.close() """ account = api._account._check_valid(account) if account is None: raise Exception(f"多账户模式下, 需要指定账户实例 account") TIME_CELL = 60 # 等时长下单的时间单元, 单位: 秒 HISTORY_DAY_LENGTH = 10 # 使用多少天的历史数据用来计算每个时间单元的下单手数 if duration % TIME_CELL or duration < 60: raise Exception(f"duration {duration} 参数应该为 {TIME_CELL} 的整数倍") pos = account.get_position(symbol) target_pos = int(target_pos) delta_pos = target_pos - pos.pos target_volume = abs(delta_pos) # 总的下单手数 if target_volume == 0: return DataFrame(columns=['interval', 'target_pos', 'price']) # 获取 Kline klines = api.get_kline_serial(symbol, TIME_CELL, data_length=int(10 * 60 * 60 / TIME_CELL * HISTORY_DAY_LENGTH)) klines["time"] = klines.datetime.apply( lambda x: datetime.fromtimestamp(x // 1000000000).time()) # k线时间 klines["date"] = klines.datetime.apply(lambda x: datetime.fromtimestamp( _get_trading_day_from_timestamp(x) // 1000000000).date()) # k线交易日 quote = api.get_quote(symbol) # 当前交易日完整的交易时间段 trading_timestamp = _get_trading_timestamp(quote, quote.datetime) trading_timestamp_nano_range = trading_timestamp[ 'night'] + trading_timestamp['day'] # 当前交易日完整的交易时间段 # 当前时间 行情时间 current_timestamp_nano = _get_trade_timestamp(quote.datetime, float('nan')) if not trading_timestamp_nano_range[0][ 0] <= current_timestamp_nano < trading_timestamp_nano_range[-1][1]: raise Exception("当前时间不在指定的交易时间段内") current_datetime = datetime.fromtimestamp(current_timestamp_nano // 1000000000) # 下一分钟的开始时间 next_datetime = current_datetime.replace(second=0) + timedelta(minutes=1) start_datetime_nano = int(next_datetime.timestamp()) * 1000000000 r = _rangeset_head( _rangeset_slice(trading_timestamp_nano_range, start_datetime_nano), int(duration * 1e9)) if not (r and trading_timestamp_nano_range[0][0] <= r[-1][-1] < trading_timestamp_nano_range[-1][1]): raise Exception("指定时间段超出当前交易日") start_datetime = datetime.fromtimestamp(start_datetime_nano // 1000000000) end_datetime = datetime.fromtimestamp((r[-1][-1] - 1) // 1000000000) time_slot_start = time(start_datetime.hour, start_datetime.minute) # 计划交易时段起始时间点 time_slot_end = time(end_datetime.hour, end_datetime.minute) # 计划交易时段终点时间点 if time_slot_end > time_slot_start: # 判断是否类似 23:00:00 开始, 01:00:00 结束这样跨天的情况 klines = klines[(klines["time"] >= time_slot_start) & (klines["time"] <= time_slot_end)] else: klines = klines[(klines["time"] >= time_slot_start) | (klines["time"] <= time_slot_end)] # 获取在预设交易时间段内的所有K线, 即时间位于 time_slot_start 到 time_slot_end 之间的数据 need_date = klines['date'].drop_duplicates()[-HISTORY_DAY_LENGTH:] klines = klines[klines['date'].isin(need_date)] grouped_datetime = klines.groupby(['date', 'time'])['volume'].sum() # 计算每个交易日内的预设交易时间段内的成交量总和(level=0: 表示按第一级索引"data"来分组)后,将每根k线的成交量除以所在交易日内的总成交量,计算其所占比例 volume_percent = grouped_datetime / grouped_datetime.groupby(level=0).sum() predicted_percent = volume_percent.groupby( level=1).mean() # 将历史上相同时间单元的成交量占比使用算数平均计算出预测值 # 计算每个时间单元的成交量预测值 time_table = DataFrame(columns=['interval', 'volume', 'price']) volume_left = target_volume # 剩余手数 percent_left = 1 # 剩余百分比 for index, value in predicted_percent.items(): volume = round(target_volume * (value / percent_left)) volume_left -= volume percent_left -= value append_time_table = pd.DataFrame([{ "interval": TIME_CELL - 2, "volume": volume, "price": "PASSIVE" }, { "interval": 2, "volume": 0, "price": "ACTIVE" }]) time_table = pd.concat([time_table, append_time_table], ignore_index=True) time_table['volume'] = time_table['volume'].mul(np.sign(delta_pos)) time_table['target_pos'] = time_table['volume'].cumsum() time_table['target_pos'] = time_table['target_pos'].add(pos.pos) time_table.drop(columns=['volume'], inplace=True) time_table = time_table.astype({ 'target_pos': 'int64', 'interval': 'float64' }) return time_table