Esempio n. 1
0
    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))
Esempio n. 2
0
    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)
Esempio n. 3
0
 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
Esempio n. 4
0
 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)
Esempio n. 5
0
    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]
Esempio n. 6
0
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
Esempio n. 7
0
    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
Esempio n. 8
0
    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"])
Esempio n. 9
0
 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)
Esempio n. 10
0
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