# coding=utf-8
# ======================================
# File:     history.py
# Author:   Jackie PENG
# Contact:  jackie.pengzhao@gmail.com
# Created:  2020-02-16
# Desc:
#   HistoryPanel Class, and more history
# data manipulating functions.
# ======================================

import pandas as pd
import numpy as np
from typing import Union, Iterable, Any, Optional, Callable

from qteasy.database import DataSource

from qteasy.utilfuncs import (
    str_to_list,
    list_to_str_format,
    list_or_slice,
    labels_to_dict,
    ffill_3d_data,
    fill_nan_data,
    fill_inf_data,
    pandas_freq_alias_version_conversion,
    regulate_date_format,
)

from qteasy.datatypes import (
    DataType,
    get_history_data_from_source,
    get_reference_data_from_source,
    infer_data_types,
)


class HistoryPanel():
    """qteasy 中用于统一管理多标的、多时间点、多数据类型历史数据的三维数据容器。

    HistoryPanel 本质是一个三维 ``numpy.ndarray``，三条轴分别表示标的（shares）、时间
    （hdates）和历史数据类型（htypes），支持按任意轴灵活切片、重标记以及与
    pandas DataFrame 之间的互相转换，并作为 get_history_data 与可视化栈
    （如 ``HistoryPanel.plot()`` 与 ``qt.candle``）之间的核心桥梁。

    更详细的结构说明（轴标签、切片示例、标签管理等）见文档「HistoryPanel 类」相关章节。
    """

    def __init__(self, values: np.ndarray = None, levels=None, rows=None, columns=None):
        """初始化 HistoryPanel 对象，并根据输入的数据与轴标签构建三维历史数据结构。

        可以在创建时同时给出标的（levels）、时间（rows）和数据类型（columns）标签，
        若未显式给出则按数据形状自动补全；当仅给出部分标签或数组维度不足三维时，会根据
        输入自动推断缺失维度并重塑为 ``(level, row, column)`` 结构。关于标签约定与
        典型创建方式，详见文档「HistoryPanel 类」章节。

        Parameters
        ----------
        values : numpy.ndarray, optional
            历史数据数组，维度不能超过三维；若维度不足三维，将根据标签数量自动补齐维度。
            为 None 或空数组时创建一个空的 HistoryPanel（其 ``is_empty`` 为 True）。
        levels : str or sequence of str, optional
            标的标签，数量应等于 ``values`` 第一维长度，每一层代表一种股票或资产。
        rows : str or sequence of str, optional
            时间标签，可以是可转换为 ``pandas.Timestamp`` 的字符串序列或 DatetimeIndex，
            每个标签对应一条时间记录。
        columns : str or sequence of str, optional
            历史数据类型标签，每一列代表一种数据类型（如 open、high、close、volume 等）。

        Returns
        -------
        HistoryPanel
            新构建的 HistoryPanel 对象。
        """

        # TODO: 在生成HistoryPanel时如果只给出data或者只给出data+columns，生成HistoryPanel打印时会报错，问题出在to_dataFrame()上
        #  在生成HistoryPanel时传入的ndarray会被直接用于HistoryPanel，如果事后修改这个ndarray，HistoryPanel也会改变
        #  应该考虑是否在创建HistoryPanel时生成ndarray的一个copy而不是使用其自身 。
        if (not isinstance(values, np.ndarray)) and (values is not None):
            raise TypeError(f'input value type should be numpy ndarray, got {type(values)}')

        self._levels = None
        self._columns = None
        self._rows = None
        if values is None or values.size == 0:
            self._l_count, self._r_count, self._c_count = (0, 0, 0)
            self._values = None
            self._is_empty = True
        else:
            assert values.ndim <= 3, \
                f'input array should be equal to or less than 3 dimensions, got {len(values.shape)}'

            if isinstance(levels, str):
                levels = str_to_list(levels)
            if isinstance(columns, str):
                columns = str_to_list(columns)
            if isinstance(rows, str):
                rows = str_to_list(rows)
            # 处理输入数据，补齐缺失的维度，根据输入数据的维度以及各维度的标签数据数量确定缺失的维度
            if values.ndim == 1:
                values = values.reshape(1, values.shape[0], 1)
            elif values.ndim == 2:
                if (levels is None) or (len(levels) == 1):
                    values = values.reshape(1, *values.shape)
                else:
                    values = values.reshape(*values.T.shape, 1)
            self._l_count, self._r_count, self._c_count = values.shape
            self._values = values
            self._is_empty = False

            # 检查输入数据的标签，处理标签数据以确认标签代表的数据各维度的数据量
            if levels is None:
                levels = range(self._l_count)
            if rows is None:
                rows = pd.date_range(periods=self._r_count, end='2020-08-08', freq='d')
            if columns is None:
                columns = range(self._c_count)

            assert (len(levels), len(rows), len(columns)) == values.shape, \
                f'ValueError, value shape does not match, input data: ({(values.shape)}) while given axis' \
                f' imply ({(len(levels), len(rows), len(columns))})'

            # 建立三个纬度的标签配对——建立标签序号字典，通过labels_to_dict()函数将标签和该纬度数据序号一一匹配
            # 首先建立层标签序号字典：
            self._levels = labels_to_dict(levels, range(self._l_count))

            # 再建立行标签序号字典，即日期序号字典，再生成字典之前，检查输入标签数据的类型，并将数据转化为pd.Timestamp格式
            assert isinstance(rows, (list, dict, pd.DatetimeIndex)), \
                f'TypeError, input_hdates should be a list or DatetimeIndex, got {type(rows)} instead'
            try:
                new_rows = [pd.to_datetime(date) for date in rows]
            except:
                raise ValueError('one or more item in hdate list can not be converted to Timestamp')
            self._rows = labels_to_dict(new_rows, range(self._r_count))

            # 建立列标签序号字典
            self._columns = labels_to_dict(columns, range(self._c_count))

    @property
    def is_empty(self):
        """判断HistoryPanel是否为空"""
        return self._is_empty

    @property
    def values(self):
        """返回HistoryPanel的values"""
        return self._values

    @property
    def levels(self):
        """返回HistoryPanel的层标签字典，也是HistoryPanel的股票代码字典
        这个字典在level的标签与level的id之间建立了一个联系，因此，如果需要通过层标签来快速地访问某一层的数据，可以非常容易地通过：
            data = HP.values[levels[level_name[a], :, :]
        来访问

        不过这是HistoryPanel内部的处理机制，在HistoryPanel的外部，可以通过切片的方式快速访问不同的数据。
        """
        return self._levels

    @property
    def shares(self):
        """返回HistoryPanel的层标签——股票列表"""
        if self.is_empty:
            return 0
        else:
            return list(self._levels.keys())

    @shares.setter
    def shares(self, input_shares):
        if not self.is_empty:
            if isinstance(input_shares, str):
                input_shares = str_to_list(input_string=input_shares)
            assert len(input_shares) == self.level_count, \
                f'ValueError, the number of input shares ({len(input_shares)}) does not match level ' \
                f'count ({self.level_count})'
            self._levels = labels_to_dict(input_shares, self.shares)

    @property
    def level_count(self):
        """返回HistoryPanel中股票或资产品种的数量"""
        return self._l_count

    @property
    def share_count(self):
        """获取HistoryPanel中股票或资产品种的数量"""
        return self._l_count

    @property
    def rows(self):
        """ 返回Hi storyPanel的日期字典，通过这个字典建立日期与行号的联系：
        因此内部可以较快地进行数据切片或数据访问

        Returns
        -------
        dict
            日期字典
        """
        return self._rows

    @property
    def hdates(self):
        """获取HistoryPanel的历史日期时间戳list"""
        # TODO: Maybe: 可以将返回值包装成一个pandas.Index对象，
        #  这样有更多方便好用的方法和属性可用
        #  例如
        #  return pd.Index(self._rows.keys())
        #  这样就可以用 HP.hdates.date / HP.hdates.where()
        #  等等方法和属性了
        #  shares 和 htypes 属性也可以如法炮制
        if self.is_empty:
            return 0
        else:
            # return pd.Index(self._rows.keys(), dtype='datetime64')
            return list(self._rows.keys())

    @hdates.setter
    def hdates(self, input_hdates: list):
        if not self.is_empty:
            if not isinstance(input_hdates, list):
                error = f'input_hdates should be a list, got {type(input_hdates)} instead'
                raise TypeError(error)
            if not len(input_hdates) == self.row_count:
                error = f'the number of input shares ({len(input_hdates)}) does not match level count ({self.row_count})'
                raise ValueError(error)
            try:
                new_hdates = [pd.to_datetime(date) for date in input_hdates]
            except Exception as e:
                error = f'{e} one or more item in hdate list can not be converted to Timestamp'
                raise ValueError(error)
            self._rows = labels_to_dict(new_hdates, self.hdates)

    @property
    def row_count(self):
        """获取HistoryPanel的行数量"""
        return self._r_count

    @property
    def hdate_count(self):
        """获取HistoryPanel的历史数据类型数量"""
        return self._r_count

    @property
    def htypes(self):
        """获取HistoryPanel的历史数据类型列表"""
        if self.is_empty:
            return 0
        else:
            return list(self._columns.keys())

    @htypes.setter
    def htypes(self, input_htypes: Union[str, list]):
        """修改HistoryPanel的历史数据类型"""
        if not self.is_empty:
            if isinstance(input_htypes, str):
                input_htypes = str_to_list(input_string=input_htypes)
            if isinstance(input_htypes, list):
                assert len(input_htypes) == self.column_count, \
                    f'ValueError, the number of input shares ({len(input_htypes)}) does not match level ' \
                    f'count ({self.column_count})'
                self._columns = labels_to_dict(input_htypes, self.htypes)
            else:
                raise TypeError(f'Expect string or list as input htypes, got {type(input_htypes)} instead')

    @property
    def columns(self):
        """返回一个字典，代表HistoryPanel的历史数据，将历史数据与列号进行对应
        这样便于内部根据股票代码对数据进行切片
        """
        return self._columns

    @property
    def column_count(self):
        """获取HistoryPanel的列数量或历史数据数量"""
        return self._c_count

    @property
    def htype_count(self):
        """获取HistoryPanel的历史数据类型数量"""
        return self._c_count

    @property
    def shape(self):
        """获取HistoryPanel的各个维度的尺寸"""
        return self._l_count, self._r_count, self._c_count

    def __len__(self):
        """获取HistoryPanel的历史数据长度

        Examples
        --------

        """
        return self._r_count

    def __getitem__(self, keys=None):
        """获取历史数据的一个切片，给定一个type、日期或股票代码, 输出相应的数据

        允许的输入包括切片形式的各种输入，包括string、数字列表或切片器对象slice()，返回切片后的ndarray对象
        允许的输入示例，第一个切片代表type切片，第二个是shares，第三个是rows：
        item_key                    output
        [1:3, :,:]                  输出第1个htype～第3个htype的所有历史数据
        [[0,1,2],:,:]:              输出第0、1、2个htype对应的所有股票全部历史数据
        [['close', 'high']]         输出close、high两个类型的所有历史数据
        [0:1]                       输出0、1两个htype的所有历史数据
        ['close,high']              输出close、high两个类型的所有历史数据
        ['close:high']              输出close、high之间所有类型的历史数据（包含close和high）
        [:,[0,1,3]]                 输出0、1、3三个股票的全部历史数据
        [:,['000100', '000120']]    输出000100、000120两只股票的所有历史数据
        [:,0:2]                     输出0、1、2三个股票的历史数据
        [:,'000100,000120']         输出000100、000120两只股票的所有历史数据

        Parameters
        ----------
        keys: list/tuple/slice
            历史数据的类型名，为空时给出所有类型的数据

        Returns
        -------
        out : ndarray
            self.value的一个切片

        Examples
        --------
        >>> hp = HistoryPanel(np.array([[[10, 20, 30, 40, 50]]*10]*3),
        ...                   levels=['000001', '000002', '000003'],
        ...                   rows=pd.date_range('2015-01-05', periods=10),
        ...                   columns=['open', 'high', 'low', 'close', 'volume'])
        >>> hp
                share 0, label: 000001
                    open  high  low  close  volume
        2015-01-05    10    20   30     40      50
        2015-01-06    10    20   30     40      50
        2015-01-07    10    20   30     40      50
        2015-01-08    10    20   30     40      50
        2015-01-09    10    20   30     40      50
        2015-01-10    10    20   30     40      50
        2015-01-11    10    20   30     40      50
        2015-01-12    10    20   30     40      50
        2015-01-13    10    20   30     40      50
        2015-01-14    10    20   30     40      50

        share 1, label: 000002
                    open  high  low  close  volume
        2015-01-05    10    20   30     40      50
        2015-01-06    10    20   30     40      50
        2015-01-07    10    20   30     40      50
        2015-01-08    10    20   30     40      50
        2015-01-09    10    20   30     40      50
        2015-01-10    10    20   30     40      50
        2015-01-11    10    20   30     40      50
        2015-01-12    10    20   30     40      50
        2015-01-13    10    20   30     40      50
        2015-01-14    10    20   30     40      50

        share 2, label: 000003
                    open  high  low  close  volume
        2015-01-05    10    20   30     40      50
        2015-01-06    10    20   30     40      50
        2015-01-07    10    20   30     40      50
        2015-01-08    10    20   30     40      50
        2015-01-09    10    20   30     40      50
        2015-01-10    10    20   30     40      50
        2015-01-11    10    20   30     40      50
        2015-01-12    10    20   30     40      50
        2015-01-13    10    20   30     40      50
        2015-01-14    10    20   30     40      50
        >>> hp['close']
        array([[[40],
                [40],
                [40],
                [40],
                [40],
                [40],
                [40],
                [40],
                [40],
                [40]],

               [[40],
                [40],
                [40],
                [40],
                [40],
                [40],
                [40],
                [40],
                [40],
                [40]],

               [[40],
                [40],
                [40],
                [40],
                [40],
                [40],
                [40],
                [40],
                [40],
                [40]]])
        >>> hp['close, open, low', '000001:000002']
        array([[[40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30]],

               [[40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30],
                [40, 10, 30]]])
        """
        # TODO: 在以前版本的qteasy中，HistoryPanel是交易策略的数据调用的核心数据结构，因此对数据切片
        #  的效率要求较高，因此在切片后直接返回一个ndarray数组，但是在2.0以后这个功能不再被需要，相反，
        #  HistoryPanel切片后返回另一个HistoryPanel更有用，因此需要修改返回值为HistorPanel。

        if self.is_empty:
            return None
        else:
            key_is_none = keys is None
            key_is_tuple = isinstance(keys, tuple)
            key_is_list = isinstance(keys, list)
            key_is_slice = isinstance(keys, slice)
            key_is_string = isinstance(keys, str)
            key_is_number = isinstance(keys, int)

            # first make sure that htypes, share_pool, and hdates are either slice or list
            htype_slice = []
            share_slice = []
            hdate_slice = []
            if key_is_tuple:
                if len(keys) == 2:
                    htype_slice, share_slice = keys
                    hdate_slice = slice(None, None, None)
                elif len(keys) == 3:
                    htype_slice, share_slice, hdate_slice = keys
            elif key_is_slice or key_is_list or key_is_string or key_is_number:  # keys is a slice or list
                htype_slice = keys
                share_slice = slice(None, None, None)
                hdate_slice = slice(None, None, None)
            elif key_is_none:
                htype_slice = slice(None, None, None)
                share_slice = slice(None, None, None)
                hdate_slice = slice(None, None, None)
            else:
                htype_slice = slice(None, None, None)
                share_slice = slice(None, None, None)
                hdate_slice = slice(None, None, None)

            # check and convert each of the slice segments to the right type: a slice or \
            # a list of indices
            htype_slice = list_or_slice(htype_slice, self.columns)
            share_slice = list_or_slice(share_slice, self.levels)
            hdate_slice = list_or_slice(hdate_slice, self.rows)

            return self.values[share_slice][:, hdate_slice][:, :, htype_slice]

    def __str__(self):
        """打印HistoryPanel"""
        res = []
        if self.is_empty:
            res.append(f'{type(self)} \nEmpty History Panel at {hex(id(self))}')
        else:
            if self.level_count <= 7:
                display_shares = self.shares
            else:
                display_shares = self.shares[0:3]
            for share in display_shares:
                res.append(f'\nshare {self.levels[share]}, label: {share}\n')
                df = self.slice_to_dataframe(share=share)
                res.append(df.__str__())
                res.append('\n')
            if self.level_count > 7:
                res.append('\n ...  \n')
                for share in self.shares[-2:]:
                    res.append(f'\nshare {self.levels[share]}, label: {share}\n')
                    df = self.slice_to_dataframe(share=share)
                    res.append(df.__str__())
                    res.append('\n')
                res.append('Only first 3 and last 3 shares are displayed\n')
        return ''.join(res)

    def __repr__(self):
        return self.__str__()

    def __add__(self, other):
        if isinstance(other, (float, int, np.ndarray)):
            self._values += other

    def __sub__(self, other):
        if isinstance(other, (float, int, np.ndarray)):
            self._values -= other

    def __mul__(self, other):
        if isinstance(other, (float, int, np.ndarray)):
            self._values *= other

    def __truediv__(self, other):
        if isinstance(other, (float, int, np.ndarray)):
            self._values /= other

    def __floordiv__(self, other):
        if isinstance(other, (float, int, np.ndarray)):
            self._values //= other

    def __mod__(self, other):
        if isinstance(other, (float, int, np.ndarray)):
            self._values %= other

    def __pow__(self, other):
        if isinstance(other, (float, int, np.ndarray)):
            self._values **= other

    def segment(self, start_date=None, end_date=None):
        """ 获取HistoryPanel的一个日期片段，start_date和end_date都是日期型数据，返回
            这两个日期之间的所有数据，返回的类型为一个HistoryPanel，包含所有share和
            htypes的数据

        Parameters
        ----------
        start_date: 开始日期
        end_date: 结束日期

        Returns
        -------
        out : HistoryPanel
            一个HistoryPanel，包含start_date到end_date之间所有share和htypes的数据

        Examples
        --------
        >>> hp = HistoryPanel(np.array([[[10, 20, 30, 40, 50]]*10]*3),
        ...                          levels=['000001', '000002', '000003'],
        ...                          rows=pd.date_range('2015-01-05', periods=10),
        ...                          columns=['open', 'high', 'low', 'close', 'volume'])
        >>> hp.segment('2015-01-07', '2015-01-10')
        share 0, label: 000100
                    open  high  low  close  volume
        2015-01-07    10    20   30     40      50
        2015-01-08    10    20   30     40      50
        2015-01-09    10    20   30     40      50
        2015-01-10    10    20   30     40      50

        share 1, label: 000200
                    open  high  low  close  volume
        2015-01-07    10    20   30     40      50
        2015-01-08    10    20   30     40      50
        2015-01-09    10    20   30     40      50
        2015-01-10    10    20   30     40      50

        share 2, label: 000300
                    open  high  low  close  volume
        2015-01-07    10    20   30     40      50
        2015-01-08    10    20   30     40      50
        2015-01-09    10    20   30     40      50
        2015-01-10    10    20   30     40      50
        """
        hdates = np.array(self.hdates)
        if start_date is None:
            start_date = hdates[0]
        if end_date is None:
            end_date = hdates[-1]
        sd = pd.to_datetime(start_date)
        ed = pd.to_datetime(end_date)
        sd_index = hdates.searchsorted(sd)
        ed_index = hdates.searchsorted(ed, side='right')
        new_dates = list(hdates[sd_index:ed_index])
        new_values = self[:, :, sd_index:ed_index]
        return HistoryPanel(new_values, levels=self.shares, rows=new_dates, columns=self.htypes)

    def isegment(self, start_index=None, end_index=None):
        """ 获取HistoryPanel的一个片段，start_index和end_index都是int数，表示日期序号，返回
            这两个序号代表的日期之间的所有数据，返回的类型为一个HistoryPanel，包含所有share和
            htypes的数据

        Parameters
        ----------
        start_index: 开始日期序号
        end_index: 结束日期序号

        Returns
        -------
        out : HistoryPanel
            一个HistoryPanel，包含start_date到end_date之间所有share和htypes的数据

        Examples
        --------
        >>> hp = HistoryPanel(np.array([[[10, 20, 30, 40, 50]]*10]*3),
        ...                          levels=['000001', '000002', '000003'],
        ...                          rows=pd.date_range('2015-01-05', periods=10),
        ...                          columns=['open', 'high', 'low', 'close', 'volume'])
        >>> hp.isegment(2, 5)
        share 0, label: 000100
                    open  high  low  close  volume
        2015-01-07    10    20   30     40      50
        2015-01-08    10    20   30     40      50
        2015-01-09    10    20   30     40      50

        share 1, label: 000200
                    open  high  low  close  volume
        2015-01-07    10    20   30     40      50
        2015-01-08    10    20   30     40      50
        2015-01-09    10    20   30     40      50

        share 2, label: 000300
                    open  high  low  close  volume
        2015-01-07    10    20   30     40      50
        2015-01-08    10    20   30     40      50
        2015-01-09    10    20   30     40      50
        """
        hdates = np.array(self.hdates)
        new_dates = list(hdates[start_index:end_index])
        new_values = self[:, :, start_index:end_index]
        return HistoryPanel(new_values, levels=self.shares, rows=new_dates, columns=self.htypes)

    def slice(self, shares=None, htypes=None):
        """ 获取HistoryPanel的一个股票或数据种类片段，shares和htypes可以为列表或逗号分隔字符
            串，表示需要获取的股票或数据的种类。

        Parameters
        ----------
        shares: str or list of str
            需要的股票列表
        htypes: str or list of str
            需要的数据类型列表

        Returns
        -------
        out : HistoryPanel
            一个HistoryPanel，包含shares和htypes中指定的股票和数据类型的数据

        Examples
        --------
        >>> hp = HistoryPanel(np.array([[[10, 20, 30, 40, 50]]*10]*3),
        ...                          levels=['000001', '000002', '000003'],
        ...                          rows=pd.date_range('2015-01-05', periods=10),
        ...                          columns=['open', 'high', 'low', 'close', 'volume'])
        """
        if self.is_empty:
            return self
        if shares is None:
            shares = self.shares
        if isinstance(shares, str):
            shares = str_to_list(shares)
        if not isinstance(shares, list):
            raise KeyError(f'wrong shares are given!')

        if htypes is None:
            htypes = self.htypes
        if isinstance(htypes, str):
            htypes = str_to_list(htypes)
        if not isinstance(htypes, list):
            raise KeyError(f'wrong htypes are given!')
        new_values = self[htypes, shares]
        return HistoryPanel(new_values, levels=shares, columns=htypes, rows=self.hdates)

    def info(self):
        """ 打印本HistoryPanel对象的信息

        Returns
        -------
        None

        Examples
        --------
        >>> hp = HistoryPanel(np.array([[[10, 20, 30, 40, 50]]*10]*3),
        ...                          levels=['000001', '000002', '000003'],
        ...                          rows=pd.date_range('2015-01-05', periods=10),
        ...                          columns=['open', 'high', 'low', 'close', 'volume'])
        >>> hp.info()
        <class 'qteasy.history.HistoryPanel'>
        History Panel at 0x12215a850
        Datetime Range: 10 entries, 2015-01-05 00:00:00 to 2015-01-14 00:00:00
        Historical Data Types (total 5 data types):
        ['open', 'high', 'low', 'close', 'volume']
        Shares (total 3 shares):
        ['000001', '000002', '000003']
        non-null values for each share and data type:
                open  high  low  close  volume
        000001    10    10   10     10      10
        000002    10    10   10     10      10
        000003    10    10   10     10      10
        memory usage: 1344 bytes
        """
        import sys
        print(f'\n{type(self)}')
        if self.is_empty:
            print(f'Empty History Panel at {hex(id(self))}')
        else:
            print(f'History Panel at {hex(id(self))}')
            if self.row_count != 0:
                print(f'Datetime Range: {self.row_count} entries, {self.hdates[0]} to {self.hdates[-1]}')
            print(f'Historical Data Types (total {self.column_count} data types):')
            if self.column_count <= 10:
                print(f'{self.htypes}')
            else:
                print(f'{self.htypes[0:3]} ... {self.htypes[-3:-1]}')
            print(f'Shares (total {self.level_count} shares):')
            if self.level_count <= 10:
                print(f'{self.shares}')
            else:
                print(f'{self.shares[0:3]} ... {self.shares[-3:-1]}')
            sum_nnan = np.sum(~np.isnan(self.values), 1)
            df = pd.DataFrame(sum_nnan, index=self.shares, columns=self.htypes)
            print('non-null values for each share and data type:')
            print(df)
            print(f'memory usage: {sys.getsizeof(self.values)} bytes\n')

    def copy(self):
        """ 返回一个新的HistoryPanel对象，其值和本对象相同"""
        # TODO: 应该考虑使用copy模块的copy(deep=True)代替下面的代码
        return HistoryPanel(values=self.values, levels=self.levels, rows=self.rows, columns=self.columns)

    def len(self):
        """ 返回HistoryPanel对象的长度，即日期个数

        Returns
        -------
        int
            日期个数

        Examples
        --------
        >>> hp = HistoryPanel(np.array([[[10, 20, 30, 40, 50]]*10]*3),
        ...                          levels=['000001', '000002', '000003'],
        ...                          rows=pd.date_range('2015-01-05', periods=10),
        ...                          columns=['open', 'high', 'low', 'close', 'volume'])
        >>> hp.len()
        10
        """
        return self.row_count

    def re_label(self,
                 shares: Union[str, list] = None,
                 htypes: Union[str, list] = None,
                 hdates: Union[str, list] = None) -> None:
        """ 给HistoryPanel对象的层、行、列标签重新赋值

        Parameters
        ----------
        shares: str or list of str
            股票列表
        htypes: str or list of str
            数据类型列表
        hdates: str or list of str
            日期列表

        Returns
        -------
        None

        Examples
        --------
        >>> hp = HistoryPanel(np.array([[[10, 20, 30, 40, 50]]*10]*3),
        ...                          levels=['000001', '000002', '000003'],
        ...                          rows=pd.date_range('2015-01-05', periods=10),
        ...                          columns=['open', 'high', 'low', 'close', 'volume'])
        >>> hp.re_label(shares=['000100', '000200', '000300'], htypes=['typeA', 'typeB', 'typeC', 'typeD', 'typeE'])
        >>> hp
        share 0, label: 000100
                    typeA  typeB  typeC  typeD  typeE
        2015-01-05     10     20     30     40     50
        2015-01-06     10     20     30     40     50
        2015-01-07     10     20     30     40     50
        2015-01-08     10     20     30     40     50
        2015-01-09     10     20     30     40     50
        2015-01-10     10     20     30     40     50
        2015-01-11     10     20     30     40     50
        2015-01-12     10     20     30     40     50
        2015-01-13     10     20     30     40     50
        2015-01-14     10     20     30     40     50

        share 1, label: 000200
                    typeA  typeB  typeC  typeD  typeE
        2015-01-05     10     20     30     40     50
        2015-01-06     10     20     30     40     50
        2015-01-07     10     20     30     40     50
        2015-01-08     10     20     30     40     50
        2015-01-09     10     20     30     40     50
        2015-01-10     10     20     30     40     50
        2015-01-11     10     20     30     40     50
        2015-01-12     10     20     30     40     50
        2015-01-13     10     20     30     40     50
        2015-01-14     10     20     30     40     50

        share 2, label: 000300
                    typeA  typeB  typeC  typeD  typeE
        2015-01-05     10     20     30     40     50
        2015-01-06     10     20     30     40     50
        2015-01-07     10     20     30     40     50
        2015-01-08     10     20     30     40     50
        2015-01-09     10     20     30     40     50
        2015-01-10     10     20     30     40     50
        2015-01-11     10     20     30     40     50
        2015-01-12     10     20     30     40     50
        2015-01-13     10     20     30     40     50
        2015-01-14     10     20     30     40     50
        """
        if not self.is_empty:
            if shares is not None:
                self.shares = shares
            if htypes is not None:
                self.htypes = htypes
            if hdates is not None:
                self.hdates = hdates

    def fillna(self, with_val: Union[int, float]):
        """ 使用with_value来填充HistoryPanel中的所有nan值

        Parameters
        ----------
        with_val: float or int
            填充的值

        Returns
        -------
        out : HistoryPanel, 填充后的HistoryPanel对象
        """
        if not self.is_empty:
            self._values = fill_nan_data(self._values, with_val)
        return self

    def fillinf(self, with_val: Union[int, float]):
        """ 使用with_value来填充HistoryPanel中的所有inf值

        Parameters
        ----------
        with_val: float or int
            填充的值

        Returns
        -------
        out : HistoryPanel, 填充后的HistoryPanel对象
        """
        if not self.is_empty:
            self._values = fill_inf_data(self._values, with_val)
        return self

    def ffill(self, init_val=np.nan):
        """ 前向填充缺失值，当历史数据中存在缺失值时，使用缺失值以前
        的最近有效数据填充缺失值

        Parameters
        ----------
        init_val: float, 如果Nan值出现在第一行时，没有前序有效数据，则使用这个值来填充，默认为np.nan

        Returns
        -------
        out : HistoryPanel, 填充后的HistoryPanel对象

        Examples
        --------
        >>> hp = HistoryPanel(np.array([[[1, 2, 3], [4, np.nan, 6]], [[np.nan, 8, 9], [np.nan, np.nan, 12]]]),
        ...                   levels=['000001', '000002'], rows=['2015-01-01', '2015-01-02'],
        ...                   columns=['open', 'high', 'low'])
        >>> hp
        share 0, label: 000001
                    open  high  low
        2015-01-01   1.0   2.0  3.0
        2015-01-02   4.0   NaN  6.0
        share 1, label: 000002
                    open  high   low
        2015-01-01   NaN   8.0   9.0
        2015-01-02   NaN   NaN  12.0

        >>> hp.ffill()
        share 0, label: 000001
                    open  high  low
        2015-01-01   1.0   2.0  3.0
        2015-01-02   4.0   2.0  6.0
        share 1, label: 000002
                    open  high   low
        2015-01-01   NaN   8.0   9.0
        2015-01-02   NaN   8.0  12.0

        >>> hp.ffill(init_val=3)
        share 0, label: 000001
                    open  high  low
        2015-01-01   1.0   2.0  3.0
        2015-01-02   4.0   2.0  6.0
        share 1, label: 000002
                    open  high   low
        2015-01-01   3.0   8.0   9.0
        2015-01-02   3.0   8.0  12.0
        """

        if not self.is_empty:
            val = self.values
            if np.all(~np.isnan(val)):
                return self
            self._values = ffill_3d_data(val, init_val)
        return self

    def join(self,
             other,
             same_shares: bool = False,
             same_htypes: bool = False,
             same_hdates: bool = False,
             fill_value: float = np.nan):
        """ 将一个HistoryPanel对象与另一个HistoryPanel对象连接起来，生成一个新的HistoryPanel：

        新HistoryPanel的行、列、层标签分别是两个原始HistoryPanel的行、列、层标签的并集，也就是说，新的HistoryPanel的行、列
        层标签完全包含两个HistoryPanel对象的对应标签。

        Parameters
        ----------
        other: HistoryPanel
            需要合并的另一个HistoryPanel
        same_shares: bool, Default False
            两个HP的shares是否相同，如果相同，可以省去shares维度的标签合并，以节省时间。默认False，
        same_htypes: bool, Default False
            两个HP的htypes是否相同，如果相同，可以省去htypes维度的标签合并，以节省时间。默认False，
        same_hdates: bool, Default False
            两个HP的hdates是否相同，如果相同，可以省去hdates维度的标签合并，以节省时间。默认False，
        fill_value: float, Default np.nan

            空数据填充值，当组合后的HP存在空数据时，应该以什么值填充，默认为np.nan

        Returns
        -------
        HistoryPanel, 一个新的History Panel对象

        Examples
        --------
        # 如果两个HistoryPanel中包含标签相同的数据，那么新的HistoryPanel中将包含调用join方法的HistoryPanel对象的相应数据。例如：
        >>> hp1 = HistoryPanel(np.array([[[8, 9, 9], [7, 5, 5], [4, 8, 4], [1, 0, 7], [8, 7, 9]],
        ...                                     [[2, 3, 3], [5, 4, 6], [2, 8, 7], [3, 3, 4], [8, 8, 7]]]),
        ...                           levels=['000200', '000300'],
        ...                           rows=pd.date_range('2020-01-01', periods=5),
        ...                           columns=['close', 'open', 'high'])
        >>> hp2 = HistoryPanel(np.array([[[8, 9, 9], [7, 5, 5], [4, 8, 4], [1, 0, 7], [8, 7, 9]],
        ...                                     [[2, 3, 3], [5, 4, 6], [2, 8, 7], [3, 3, 4], [8, 8, 7]]]),
        ...                           levels=['000400', '000500'],
        ...                           rows=pd.date_range('2020-01-01', periods=5),
        ...                           columns=['close', 'open', 'high'])
        >>> hp1
        share 0, label: 000200
                    close  open  high
        2020-01-01      8     9     9
        2020-01-02      7     5     5
        2020-01-03      4     8     4
        2020-01-04      1     0     7
        2020-01-05      8     7     9
        share 1, label: 000300
                    close  open  high
        2020-01-01      2     3     3
        2020-01-02      5     4     6
        2020-01-03      2     8     7
        2020-01-04      3     3     4
        2020-01-05      8     8     7
        >>> hp2
        share 0, label: 000400
                    close  open  high
        2020-01-01      8     9     9
        2020-01-02      7     5     5
        2020-01-03      4     8     4
        2020-01-04      1     0     7
        2020-01-05      8     7     9
        share 1, label: 000500
                    close  open  high
        2020-01-01      2     3     3
        2020-01-02      5     4     6
        2020-01-03      2     8     7
        2020-01-04      3     3     4
        2020-01-05      8     8     7

        >>> hp1.join(hp2)
        share 0, label: 000200
        """

        assert isinstance(other, HistoryPanel), \
            f'TypeError, HistoryPanel can only be joined with other HistoryPanel.'
        if self.is_empty:
            return other
        elif other.is_empty:
            return self
        else:
            other_shares = other.shares
            other_htypes = other.htypes
            other_hdates = other.hdates
            this_shares = self.shares
            this_htypes = self.htypes
            this_hdates = self.hdates
            if not same_shares:
                combined_shares = list(set(this_shares).union(set(other_shares)))
                combined_shares.sort()
            else:
                assert this_shares == other_shares, f'Assertion Error, shares of two HistoryPanels are different!'
                combined_shares = self.shares
            if not same_htypes:
                combined_htypes = list(set(this_htypes).union(set(other_htypes)))
                combined_htypes.sort()
            else:
                assert this_htypes == other_htypes, f'Assertion Error, htypes of two HistoryPanels are different!'
                combined_htypes = self.htypes
            if not same_hdates:
                combined_hdates = list(set(this_hdates).union(set(other_hdates)))
                combined_hdates.sort()
            else:
                assert this_hdates == other_hdates, f'Assertion Error, hdates of two HistoryPanels are different!'
                combined_hdates = self.hdates
            combined_values = np.empty(shape=(len(combined_shares),
                                              len(combined_hdates),
                                              len(combined_htypes)))
            combined_values.fill(fill_value)
            if same_shares:
                if same_htypes:
                    for hdate in combined_hdates:
                        combined_hdate_id = labels_to_dict(combined_hdates, combined_hdates)
                        this_hdate_id = labels_to_dict(this_hdates, this_hdates)
                        other_hdate_id = labels_to_dict(other_hdates, other_hdates)
                        if hdate in this_hdates:
                            combined_values[:, combined_hdate_id[hdate], :] = self.values[:, this_hdate_id[hdate], :]
                        else:
                            combined_values[:, combined_hdate_id[hdate], :] = other.values[:, other_hdate_id[hdate], :]
                elif same_hdates:
                    for htype in combined_htypes:
                        combined_htype_id = labels_to_dict(combined_htypes, combined_htypes)
                        this_htype_id = labels_to_dict(this_htypes, this_htypes)
                        other_htype_id = labels_to_dict(other_htypes, other_htypes)
                        if htype in this_htypes:
                            combined_values[:, :, combined_htype_id[htype]] = self.values[:, :, this_htype_id[htype]]
                        else:
                            combined_values[:, :, combined_htype_id[htype]] = other.values[:, :, other_htype_id[htype]]
                else:
                    for hdate in combined_hdates:
                        for htype in combined_htypes:
                            combined_hdate_id = labels_to_dict(combined_hdates, combined_hdates)
                            this_hdate_id = labels_to_dict(this_hdates, this_hdates)
                            other_hdate_id = labels_to_dict(other_hdates, other_hdates)
                            combined_htype_id = labels_to_dict(combined_htypes, combined_htypes)
                            this_htype_id = labels_to_dict(this_htypes, this_htypes)
                            other_htype_id = labels_to_dict(other_htypes, other_htypes)
                            if htype in this_htypes and hdate in this_hdates:
                                combined_values[:, combined_hdate_id[hdate], combined_htype_id[htype]] = \
                                    self.values[:, this_hdate_id[hdate], this_htype_id[htype]]
                            elif htype in other_htypes and hdate in other_hdates:
                                combined_values[:, combined_hdate_id[hdate], combined_htype_id[htype]] = \
                                    other.values[:, other_hdate_id[hdate], other_htype_id[htype]]
            # TODO: implement this section 实现相同htype的HistoryPanel合并
            elif same_htypes:
                raise NotImplementedError
            # TODO: implement this section 实现相同数据历史时间戳的HistoryPanel合并
            else:
                raise NotImplementedError
            return HistoryPanel(values=combined_values,
                                levels=combined_shares,
                                rows=combined_hdates,
                                columns=combined_htypes)

    def as_type(self, dtype):
        """ 将HistoryPanel的数据类型转换为dtype类型，dtype只能为'float'或'int'

        Parameters
        ----------
        dtype: str, {'float', 'int'}
            需要转换的目标数据类型

        Returns
        -------
        self

        Raises
        ______
        AssertionError
            当输入的数据类型不正确或输入除float/int外的其他数据类型时
        """
        ALL_DTYPES = ['float', 'int']
        if not self.is_empty:
            assert isinstance(dtype, str), f'InputError, dtype should be a string, got {type(dtype)}'
            assert dtype in ALL_DTYPES, f'data type {dtype} is not recognized or not supported!'
            self._values = self.values.astype(dtype)
        return self

    def slice_to_dataframe(self,
                           htype: Union[str, int] = None,
                           share: Union[str, int] = None,
                           dropna: bool = False,
                           inf_as_na: bool = False) -> pd.DataFrame:
        """ 将HistoryPanel对象中的指定片段转化为DataFrame

        指定htype或者share，将这个htype或share对应的数据切片转化为一个DataFrame。
        由于HistoryPanel对象包含三维数据，因此在转化时必须指定htype或者share参数中的一个

        Parameters
        ----------
        htype:   str or int，
            表示需要生成DataFrame的数据类型切片
            如果给出此参数，定位该htype对应的切片后，将该htype对应的所有股票所有日期的数据转化为一个DataFrame
            如果类型为str，表示htype的名称，如果类型为int，代表该htype所在的列序号
        share:   str or int，
            表示需要生成DataFrame的股票代码切片
            如果给出此参数，定位该share对应的切片后，将该share对应的所有数据类型所有日期的数据转化为一个DataFrame
            如果类型为str，表示股票代码，如果类型为int，代表该share所在的层序号
        dropna: bool, Default False
            是否去除NaN值
        inf_as_na: bool, Default False
            是否将inf值当成NaN值一同去掉，当dropna为False时无效

        Returns
        -------
        pandas.DataFrame

        Examples
        --------
        >>> hp = HistoryPanel(values=np.array([[[1, 2, np.nan], [4, 5, 6]],
        ...                                    [[7, 8, np.nan], [np.inf, 11, 12]]]),
        ...                   levels=['000001', '000002'],
        ...                   rows=['2019-01-01', '2019-01-02'],
        ...                   columns=['open', 'high', 'low']))
        >>> hp
        share 0, label: 000001
                    open  high  low
        2019-01-01   1.0   2.0  NaN
        2019-01-02   4.0   5.0  NaN
        share 1, label: 000002
                    open  high   low
        2019-01-01   7.0   8.0   9.0
        2019-01-02   inf  11.0  12.0

        >>> hp.slice_to_dataframe(htype='open')
        000001  000002
        2019-01-01  1.0  7.0
        2019-01-02  4.0  inf

        >>> hp.slice_to_dataframe(share='000001')
        open  high  low
        2019-01-01  1.0  2.0  NaN
        2019-01-02  4.0  5.0  6.0

        >>> hp.slice_to_dataframe(htype='low', dropna=True)
                    000001  000002
        2019-01-02     6.0    12.0

        """

        if self.is_empty:
            return pd.DataFrame()
        if all(par is not None for par in (htype, share)) or all(par is None for par in (htype, share)):
            # 两个参数都是非None或都是None，应该弹出警告信息
            raise KeyError(f'Only and exactly one of the parameters htype and share should be given, '
                           f'got both or none')
        res_df = pd.DataFrame()
        if htype is not None:
            assert isinstance(htype, (str, int)), f'htype must be a string or an integer, got {type(htype)}'
            if isinstance(htype, int):
                htype = self.htypes[htype]
            if not htype in self.htypes:
                raise KeyError(f'htype {htype} is not found!')
            # 在生成DataFrame之前，需要把数据降低一个维度，例如shape(1, 24, 5) -> shape(24, 5)
            v = self[htype].T
            v = v.reshape(v.shape[-2:])
            res_df = pd.DataFrame(v, index=self.hdates, columns=self.shares)

        if share is not None:
            assert isinstance(share, (str, int)), f'share must be a string or an integer, got {type(share)}'
            if isinstance(share, int):
                share = self.shares[share]
            if not share in self.shares:
                raise KeyError(f'share {share} is not found!')
            # 在生成DataFrame之前，需要把数据降低一个维度，例如shape(1, 24, 5) -> shape(24, 5)
            v = self[:, share]
            v = v.reshape(v.shape[-2:])
            res_df = pd.DataFrame(v, index=self.hdates, columns=self.htypes)

        if dropna and inf_as_na:
            with pd.option_context('mode.use_inf_as_na', True):
                return res_df.dropna(how='all')
        if dropna:
            return res_df.dropna(how='all')

        return res_df

    def flatten_to_dataframe(self, along='row'):
        """ 将一个HistoryPanel"展平"成为一个DataFrame

        HistoryPanel的多层数据会被"平铺"到DataFrame的列，变成一个MultiIndex，或者多层数据
        会被平铺到DataFrame的行，同样变成一个MultiIndex，平铺到行还是列取决于along参数

        Parameters
        ----------
        along: str, {'col', 'row', 'column'} Default: 'row'
            平铺HistoryPanel的每一层时，沿行方向还是列方向平铺，
            'col'或'column'表示沿列方向平铺，'row'表示沿行方向平铺

        Returns
        -------
        pandas.DataFrame

        Examples
        --------
        >>> hp = HistoryPanel(np.array([[[12.3, 12.5, 1020010], [12.6, 13.2, 1020020]],
        ...                                    [[2.3, 2.5, 20010], [2.6, 3.2, 20020]]]),
        ...                          levels=['000300', '000001'],
        ...                          rows=['2020-01-01', '2020-01-02'],
        ...                          columns=['close', 'open', 'vol'])
        >>> hp
        share 0, label: 000300
                    close  open        vol
        2020-01-01   12.3  12.5  1020010.0
        2020-01-02   12.6  13.2  1020020.0
        share 1, label: 000001
                    close  open      vol
        2020-01-01    2.3   2.5  20010.0
        2020-01-02    2.6   3.2  20020.0

        >>> hp.flatten_to_dataframe(along='col')
                   000300                  000001
                    close  open        vol  close open      vol
        2020-01-01   12.3  12.5  1020010.0    2.3  2.5  20010.0
        2020-01-02   12.6  13.2  1020020.0    2.6  3.2  20020.0

        >>> hp.flatten_to_dataframe(along='row')
                           close  open        vol
        000300 2020-01-01   12.3  12.5  1020010.0
               2020-01-02   12.6  13.2  1020020.0
        000001 2020-01-01    2.3   2.5    20010.0
               2020-01-02    2.6   3.2    20020.0
        """
        if not isinstance(along, str):
            raise TypeError(f'along must be a string, got {type(along)} instead')
        if along not in ('col', 'row', 'column'):
            raise ValueError(f'along must be "col" or "row", got {along}')

        df_dict = self.to_df_dict(by='share')
        if self.is_empty:
            return pd.DataFrame()
        if along in ('col', 'column'):
            return pd.concat(df_dict, axis=1, keys=df_dict.keys())
        if along == 'row':
            return pd.concat(df_dict, axis=0, keys=df_dict.keys())

    def to_share_frame(self, share: Union[str, int]) -> pd.DataFrame:
        """ 将单一股票在 HistoryPanel 中的所有数据类型切片为一个 DataFrame。

        该方法是 ``slice_to_dataframe(share=...)`` 的语法糖，返回的 DataFrame
        以时间为索引、以全部 ``htypes`` 为列，适合做单股票全指标分析。

        Parameters
        ----------
        share : str or int
            股票代码或层序号，语义与 ``slice_to_dataframe(share=...)`` 中的 ``share``
            参数一致。

        Returns
        -------
        pandas.DataFrame
            行索引为 ``hdates``，列为 ``htypes``，包含该股票在所有时间点上的全部
            历史数据。
        """
        return self.slice_to_dataframe(share=share)

    def to_multi_index_dataframe(self, along=None):
        """ 等同于HistoryPanel.flatten_to_dataframe()

        Parameters
        ----------
        along: str, {'col', 'row', 'column'} Default: 'row'
            平铺HistoryPanel的每一层时，沿行方向还是列方向平铺，
            'col'或'column'表示沿列方向平铺，'row'表示沿行方向平铺

        Returns
        -------
        pandas.DataFrame

        Examples
        --------
        >>> hp = HistoryPanel(np.array([[[12.3, 12.5, 1020010], [12.6, 13.2, 1020020]],
        ...                                    [[2.3, 2.5, 20010], [2.6, 3.2, 20020]]]),
        ...                          levels=['000300', '000001'],
        ...                          rows=['2020-01-01', '2020-01-02'],
        ...                          columns=['close', 'open', 'vol'])
        >>> hp
        share 0, label: 000300
                    close  open        vol
        2020-01-01   12.3  12.5  1020010.0
        2020-01-02   12.6  13.2  1020020.0
        share 1, label: 000001
                    close  open      vol
        2020-01-01    2.3   2.5  20010.0
        2020-01-02    2.6   3.2  20020.0

        >>> hp.to_multi_index_dataframe(along='col')
                   000300                  000001
                    close  open        vol  close open      vol
        2020-01-01   12.3  12.5  1020010.0    2.3  2.5  20010.0
        2020-01-02   12.6  13.2  1020020.0    2.6  3.2  20020.0

        >>> hp.to_multi_index_dataframe(along='row')
                           close  open        vol
        000300 2020-01-01   12.3  12.5  1020010.0
               2020-01-02   12.6  13.2  1020020.0
        000001 2020-01-01    2.3   2.5    20010.0
               2020-01-02    2.6   3.2    20020.0
        """
        return self.flatten_to_dataframe(along=along)

    def flatten(self, along=None):
        """ 等同于HistoryPanel.flatten_to_dataframe()

        Parameters
        ----------
        along: str, {'col', 'row', 'column'} Default: 'row'
            平铺HistoryPanel的每一层时，沿行方向还是列方向平铺，
            'col'或'column'表示沿列方向平铺，'row'表示沿行方向平铺

        Returns
        -------
        pandas.DataFrame

        Examples
        --------
        >>> hp = HistoryPanel(np.array([[[12.3, 12.5, 1020010], [12.6, 13.2, 1020020]],
        ...                                    [[2.3, 2.5, 20010], [2.6, 3.2, 20020]]]),
        ...                          levels=['000300', '000001'],
        ...                          rows=['2020-01-01', '2020-01-02'],
        ...                          columns=['close', 'open', 'vol'])
        >>> hp
        share 0, label: 000300
                    close  open        vol
        2020-01-01   12.3  12.5  1020010.0
        2020-01-02   12.6  13.2  1020020.0
        share 1, label: 000001
                    close  open      vol
        2020-01-01    2.3   2.5  20010.0
        2020-01-02    2.6   3.2  20020.0

        >>> hp.flatten(along='col')
                   000300                  000001
                    close  open        vol  close open      vol
        2020-01-01   12.3  12.5  1020010.0    2.3  2.5  20010.0
        2020-01-02   12.6  13.2  1020020.0    2.6  3.2  20020.0

        >>> hp.flatten(along='row')
                           close  open        vol
        000300 2020-01-01   12.3  12.5  1020010.0
               2020-01-02   12.6  13.2  1020020.0
        000001 2020-01-01    2.3   2.5    20010.0
               2020-01-02    2.6   3.2    20020.0
        """
        return self.flatten_to_dataframe(along=along)

    def mean(self, by: str = 'share', skipna: bool = True) -> pd.DataFrame:
        """计算HistoryPanel的均值统计。"""
        if self.is_empty:
            return pd.DataFrame()
        if by not in ('share', 'htype'):
            raise ValueError(f'parameter "by" must be "share" or "htype", got {by}')

        values = self.values.astype(float)
        if skipna:
            agg_share = np.nanmean(values, axis=1)
        else:
            agg_share = values.mean(axis=1)
        df_share = pd.DataFrame(agg_share, index=self.shares, columns=self.htypes)
        if by == 'share':
            return df_share
        return df_share.T

    def std(self, by: str = 'share', skipna: bool = True) -> pd.DataFrame:
        """计算HistoryPanel的标准差统计，ddof=1。"""
        if self.is_empty:
            return pd.DataFrame()
        if by not in ('share', 'htype'):
            raise ValueError(f'parameter "by" must be "share" or "htype", got {by}')

        values = self.values.astype(float)
        if skipna:
            agg_share = np.nanstd(values, axis=1, ddof=1)
        else:
            agg_share = values.std(axis=1, ddof=1)
        df_share = pd.DataFrame(agg_share, index=self.shares, columns=self.htypes)
        if by == 'share':
            return df_share
        return df_share.T

    def min(self, by: str = 'share', skipna: bool = True) -> pd.DataFrame:
        """计算HistoryPanel的最小值统计。"""
        if self.is_empty:
            return pd.DataFrame()
        if by not in ('share', 'htype'):
            raise ValueError(f'parameter "by" must be "share" or "htype", got {by}')

        values = self.values.astype(float)
        if skipna:
            agg_share = np.nanmin(values, axis=1)
        else:
            agg_share = values.min(axis=1)
        df_share = pd.DataFrame(agg_share, index=self.shares, columns=self.htypes)
        if by == 'share':
            return df_share
        return df_share.T

    def max(self, by: str = 'share', skipna: bool = True) -> pd.DataFrame:
        """计算HistoryPanel的最大值统计。"""
        if self.is_empty:
            return pd.DataFrame()
        if by not in ('share', 'htype'):
            raise ValueError(f'parameter "by" must be "share" or "htype", got {by}')

        values = self.values.astype(float)
        if skipna:
            agg_share = np.nanmax(values, axis=1)
        else:
            agg_share = values.max(axis=1)
        df_share = pd.DataFrame(agg_share, index=self.shares, columns=self.htypes)
        if by == 'share':
            return df_share
        return df_share.T

    def describe(
            self,
            by: Optional[str] = 'share',
            percentiles: tuple = (0.25, 0.5, 0.75),
            include: str = 'numeric',
            ddof: int = 1,
    ) -> pd.DataFrame:
        """对HistoryPanel进行基础统计描述。"""
        if self.is_empty:
            return pd.DataFrame()
        # 目前仅支持数值型统计，兼容老版本 pandas，这里不直接将 include 透传给 pandas.describe
        if include not in ('numeric', None):
            raise ValueError('only numeric include is supported for HistoryPanel.describe')

        # by='share'：对每只股票的 DataFrame 做 describe，拼接为 MultiIndex 列
        if by == 'share':
            rows = {}
            col_tuples = []
            first = True
            for share in self.shares:
                df = self.slice_to_dataframe(share=share)
                # DataFrame 仅包含数值列，不再传递 include 参数，以兼容当前 pandas 版本
                desc = df.describe(percentiles=percentiles)
                if first:
                    stats_order = list(desc.index)
                    for htype in df.columns:
                        for stat in stats_order:
                            col_tuples.append((htype, stat))
                    first = False
                values = []
                for htype in df.columns:
                    for stat in stats_order:
                        values.append(desc.loc[stat, htype])
                rows[share] = values
            desc_df = pd.DataFrame.from_dict(rows, orient='index')
            desc_df.columns = pd.MultiIndex.from_tuples(col_tuples, names=['htype', 'stat'])
            return desc_df

        # by='htype'：每个 htype 在所有股票与时间上的分布
        if by == 'htype':
            rows = {}
            stats_index = None
            for i, htype in enumerate(self.htypes):
                arr = self.values[:, :, i].astype(float).ravel()
                arr = arr[~np.isnan(arr)]
                series = pd.Series(arr)
                desc = series.describe(percentiles=percentiles)
                if stats_index is None:
                    stats_index = desc.index
                rows[htype] = desc.values
            desc_df = pd.DataFrame.from_dict(rows, orient='index', columns=stats_index)
            return desc_df

        # 全局 describe：所有数值视作一个整体样本池
        if by is None:
            arr = self.values.astype(float).ravel()
            arr = arr[~np.isnan(arr)]
            series = pd.Series(arr)
            desc = series.describe(percentiles=percentiles)
            if 'std' in desc.index:
                desc['std'] = series.std(ddof=ddof)
            return desc.to_frame().T

        raise ValueError(f'parameter \"by\" must be \"share\", \"htype\" or None, got {by}')

    def rolling(
            self,
            window: int,
            min_periods: Optional[int] = None,
            center: bool = False,
            by: str = 'share',
    ) -> "HistoryPanelRolling":
        """基于 HistoryPanel 构造滚动窗口统计对象。

        滚动仅沿时间轴（rows / hdates）进行，``window`` 为整数 bar 数。

        Parameters
        ----------
        window : int
            滚动窗口长度。
        min_periods : int, optional
            最小有效观测数，小于该数时结果为 ``NaN``。默认与 ``window`` 相同。
        center : bool, default False
            是否使用居中窗口，语义与 ``pandas.Series.rolling`` 一致。
        by : {'share', 'htype'}, default 'share'
            指定滚动的分组方式：
            - 'share': 每只股票的每个 htype 独立做滚动统计（最常用）；
            - 'htype': 每个 htype 在所有股票上独立做滚动统计。
        """
        if self.is_empty:
            return HistoryPanelRolling(self, window, min_periods, center, by)
        if not isinstance(window, int) or window <= 0:
            raise ValueError(f'window must be a positive integer, got {window}')
        if min_periods is None:
            min_periods = window
        if not isinstance(min_periods, int) or min_periods <= 0:
            raise ValueError(f'min_periods must be a positive integer, got {min_periods}')
        if by not in ('share', 'htype'):
            raise ValueError(f'parameter \"by\" must be \"share\" or \"htype\", got {by}')
        return HistoryPanelRolling(self, window, min_periods, center, by)

    def returns(
            self,
            price_htype: str = 'close',
            method: str = 'simple',
            periods: int = 1,
            as_panel: bool = False,
            dropna: bool = False,
    ):
        """基于指定价格序列计算收益率。

        Parameters
        ----------
        price_htype : str, default 'close'
            用于计算收益率的价格类型，必须在 htypes 中存在。
        method : {'simple', 'log'}, default 'simple'
            - simple: r_t = p_t / p_{t-periods} - 1
            - log: r_t = log(p_t) - log(p_{t-periods})
        periods : int, default 1
            收益率间隔的 bar 数。
        as_panel : bool, default False
            False 返回 DataFrame（index=时间，columns=shares）；
            True 返回 HistoryPanel（htypes 仅含 ret_{price_htype}）。
        dropna : bool, default False
            True 时删除全为 NaN 的起始行。

        Returns
        -------
        pandas.DataFrame or HistoryPanel
        """
        if self.is_empty:
            if as_panel:
                return HistoryPanel()
            return pd.DataFrame()
        if price_htype not in self.htypes:
            raise ValueError(f'price_htype "{price_htype}" not found in htypes: {self.htypes}')
        if method not in ('simple', 'log'):
            raise ValueError(f'method must be "simple" or "log", got {method}')

        ci = self.htypes.index(price_htype)
        prices = self.values[:, :, ci].astype(float)  # (shares, times)

        n_share, n_time = prices.shape
        ret = np.full((n_share, n_time), np.nan, dtype=float)

        for i in range(n_share):
            p = prices[i, :]
            for t in range(periods, n_time):
                p_prev = p[t - periods]
                p_curr = p[t]
                if np.isnan(p_prev) or np.isnan(p_curr) or p_prev <= 0:
                    ret[i, t] = np.nan
                elif method == 'simple':
                    ret[i, t] = p_curr / p_prev - 1.0
                else:  # log
                    ret[i, t] = np.log(p_curr) - np.log(p_prev)

        if dropna:
            # 删除整行全 NaN 的起始行
            mask = np.any(~np.isnan(ret), axis=0)
            ret = ret[:, mask]
            used_hdates = [d for d, m in zip(self.hdates, mask) if m]
        else:
            used_hdates = list(self.hdates)

        if as_panel:
            new_htype = f'ret_{price_htype}'
            return HistoryPanel(
                values=ret.reshape(n_share, -1, 1),
                levels=self.shares,
                rows=used_hdates,
                columns=[new_htype],
            )
        df = pd.DataFrame(ret.T, index=used_hdates, columns=self.shares)
        return df

    def volatility(
            self,
            window: int,
            price_htype: str = 'close',
            method: str = 'simple',
            annualize: bool = True,
            periods_per_year: Optional[int] = None,
            as_panel: bool = False,
    ):
        """基于收益率序列计算滚动波动率（标准差）。

        Parameters
        ----------
        window : int
            滚动窗口长度（bar 数）。
        price_htype : str, default 'close'
            用于计算收益率的价格类型。
        method : {'simple', 'log'}, default 'simple'
            收益率计算方式，与 returns() 一致。
        annualize : bool, default True
            是否年化（乘以 sqrt(periods_per_year)）。
        periods_per_year : int, optional
            年化时每年 bar 数；未指定且 annualize=True 时尝试从时间间隔推断，无法推断则报错。
        as_panel : bool, default False
            返回形式同 returns()。

        Returns
        -------
        pandas.DataFrame or HistoryPanel
        """
        if self.is_empty:
            if as_panel:
                return HistoryPanel()
            return pd.DataFrame()
        ret_df = self.returns(price_htype=price_htype, method=method, periods=1, as_panel=False, dropna=False)
        # 滚动标准差：为了在第 window 行即可得到第一个非 NaN 结果，
        # 这里使用 min_periods=1，让 NaN 收益率自然通过跳过逻辑处理。
        vol_df = ret_df.rolling(window=window, min_periods=1).std()

        if annualize:
            if periods_per_year is not None:
                if not isinstance(periods_per_year, (int, float)) or periods_per_year <= 0:
                    raise ValueError(f'periods_per_year must be a positive number, got {periods_per_year}')
                scale = np.sqrt(periods_per_year)
            else:
                # 尝试从 hdates 推断：按平均间隔估算每年 bar 数
                if len(self.hdates) < 2:
                    raise ValueError('cannot infer periods_per_year from fewer than 2 dates; set periods_per_year explicitly')
                try:
                    idx = pd.DatetimeIndex(self.hdates)
                    delta = idx[-1] - idx[0]
                    n_bars = max(1, len(idx) - 1)
                    avg_delta = delta / n_bars
                    days = avg_delta.total_seconds() / 86400.0
                    if days >= 1:
                        scale = np.sqrt(252.0 / days)  # 日频约 252
                    elif days * 5 >= 1:
                        scale = np.sqrt(52.0 / (days * 5))  # 周频约 52
                    else:
                        scale = np.sqrt(252.0 * (1.0 / days))
                except Exception as e:
                    raise ValueError(f'could not infer periods_per_year from hdates: {e}; set periods_per_year explicitly')
            vol_df = vol_df * scale

        if as_panel:
            vol_arr = vol_df.values.T  # (shares, times)
            n_share, n_time = vol_arr.shape
            return HistoryPanel(
                values=vol_arr.reshape(n_share, n_time, 1),
                levels=self.shares,
                rows=list(vol_df.index),
                columns=[f'vol_{window}'],
            )
        return vol_df

    @property
    def kline(self) -> "_HistoryPanelKlineAccessor":
        """K 线技术指标访问器，提供 sma、ema、bbands、macd、kdj 等方法。"""
        return _HistoryPanelKlineAccessor(self)

    def alpha_beta(
            self,
            benchmark: Union[pd.Series, pd.DataFrame],
            price_htype: str = 'close',
            method: str = 'simple',
            freq: Optional[str] = None,
            annualize: bool = True,
    ) -> pd.DataFrame:
        """计算各股票相对于给定基准收益率序列的 alpha / beta 等指标。

        Parameters
        ----------
        benchmark : Series or DataFrame
            基准价格时间序列，index 应与 HistoryPanel.hdates 对齐或至少有交集。
            DataFrame 时只使用第一列作为基准价格。
        price_htype : str, default 'close'
            用于计算收益率的价格类型。
        method : {'simple', 'log'}, default 'simple'
            收益率计算方式，与 returns() 一致。
        freq : str, optional
            收益率频率字符串，用于 alpha 年化时推断每年 bar 数，如 'D'、'W'、'M'。
        annualize : bool, default True
            是否对 alpha 进行年化。

        Returns
        -------
        pandas.DataFrame
            index 为 shares，列为 ['alpha', 'beta', 'r2', 'n_obs']。
        """
        if self.is_empty:
            return pd.DataFrame(columns=['alpha', 'beta', 'r2', 'n_obs'])

        if not isinstance(benchmark, (pd.Series, pd.DataFrame)):
            raise TypeError('benchmark must be pandas Series or DataFrame')

        if isinstance(benchmark, pd.DataFrame):
            if benchmark.shape[1] == 0:
                raise ValueError('benchmark DataFrame has no columns')
            bench_price = benchmark.iloc[:, 0]
        else:
            bench_price = benchmark

        # 对齐时间索引
        bench_price = bench_price.astype(float)
        stock_ret = self.returns(price_htype=price_htype, method=method, periods=1, as_panel=False, dropna=False)

        # 基准收益率
        bench_ret = bench_price.copy()
        bench_ret.iloc[:] = np.nan
        for i in range(1, len(bench_price)):
            p_prev = bench_price.iloc[i - 1]
            p_curr = bench_price.iloc[i]
            if np.isnan(p_prev) or np.isnan(p_curr) or p_prev <= 0:
                bench_ret.iloc[i] = np.nan
            elif method == 'simple':
                bench_ret.iloc[i] = p_curr / p_prev - 1.0
            else:
                bench_ret.iloc[i] = np.log(p_curr) - np.log(p_prev)

        # 按共同日期对齐
        common_index = stock_ret.index.intersection(bench_ret.index)
        stock_ret = stock_ret.loc[common_index]
        bench_ret = bench_ret.loc[common_index]

        # 每年 bar 数，用于 alpha 年化
        if annualize:
            if freq is not None:
                freq_u = freq.upper()
                if freq_u.startswith('D'):
                    per_year = 252.0
                elif freq_u.startswith('W'):
                    per_year = 52.0
                elif freq_u.startswith('M'):
                    per_year = 12.0
                else:
                    raise ValueError(f'unsupported freq \"{freq}\" for annualization')
            else:
                per_year = 252.0
        else:
            per_year = 1.0

        res = {'alpha': [], 'beta': [], 'r2': [], 'n_obs': []}
        for share in self.shares:
            if share not in stock_ret.columns:
                res['alpha'].append(np.nan)
                res['beta'].append(np.nan)
                res['r2'].append(np.nan)
                res['n_obs'].append(0)
                continue
            y = stock_ret[share]
            x = bench_ret
            mask = (~y.isna()) & (~x.isna())
            xv = x[mask].to_numpy()
            yv = y[mask].to_numpy()
            n = len(xv)
            if n < 2:
                res['alpha'].append(np.nan)
                res['beta'].append(np.nan)
                res['r2'].append(np.nan)
                res['n_obs'].append(n)
                continue

            # 线性回归 y = alpha + beta * x
            beta, alpha = np.polyfit(xv, yv, 1)
            # 拟合优度 R^2
            y_hat = alpha + beta * xv
            ss_res = np.sum((yv - y_hat) ** 2)
            ss_tot = np.sum((yv - np.mean(yv)) ** 2)
            r2 = 1.0 - ss_res / ss_tot if ss_tot > 0 else np.nan

            res['alpha'].append(alpha * per_year)
            res['beta'].append(beta)
            res['r2'].append(r2)
            res['n_obs'].append(n)

        result = pd.DataFrame(res, index=self.shares)
        return result

    def apply_ta(
            self,
            func_name: str,
            htype: str = 'close',
            shares: Optional[Iterable[str]] = None,
            as_panel: bool = True,
            **kwargs,
    ):
        """调用 qteasy.tafuncs 中的技术指标函数，并在多股票上广播计算。

        Parameters
        ----------
        func_name : str
            qteasy.tafuncs 中的函数名称，如 'sma'、'ema' 等。
        htype : str, default 'close'
            作为输入的一维时间序列的数据类型。
        shares : list of str, optional
            需要计算的股票列表，默认使用全部 shares。
        as_panel : bool, default True
            True 时返回新的 HistoryPanel，在 htypes 末尾追加输出列；
            False 时返回 MultiIndex 列的 DataFrame（时间×[share, output_name]）。
        """
        if self.is_empty:
            return HistoryPanel() if as_panel else pd.DataFrame()
        import qteasy.tafuncs as tafuncs
        if not hasattr(tafuncs, func_name):
            raise ValueError(f'technical indicator function \"{func_name}\" not found in qteasy.tafuncs')
        func = getattr(tafuncs, func_name)

        if htype not in self.htypes:
            raise ValueError(f'htype \"{htype}\" not found in HistoryPanel.htypes: {self.htypes}')

        if shares is None:
            share_list = list(self.shares)
        else:
            if isinstance(shares, str):
                share_list = str_to_list(shares)
            else:
                share_list = list(shares)

        ci = self.htypes.index(htype)
        idx = pd.to_datetime(self.hdates)

        # 收集各 share 的结果
        result_arrays = {}
        out_names = None
        for share in share_list:
            if share not in self.shares:
                raise ValueError(f'share \"{share}\" not found in HistoryPanel.shares')
            li = self.shares.index(share)
            series = pd.Series(self.values[li, :, ci].astype(float), index=idx)
            out = func(series, **kwargs)
            if isinstance(out, (list, tuple)):
                cols = [f'{func_name}_{i}' for i in range(len(out))]
                arrs = [np.asarray(o, dtype=float).ravel() for o in out]
            else:
                cols = [func_name]
                arrs = [np.asarray(out, dtype=float).ravel()]
            if out_names is None:
                out_names = cols
            result_arrays[share] = arrs

        if as_panel:
            # 在原 Panel 后追加新 htypes
            base = self.values.astype(float)
            n_share, n_time, _ = base.shape
            new_cols = list(self.htypes) + list(out_names)
            add_values = np.zeros((n_share, n_time, len(out_names)), dtype=float)
            add_values[:] = np.nan
            for share, arrs in result_arrays.items():
                li = self.shares.index(share)
                for j, arr in enumerate(arrs):
                    L = min(n_time, len(arr))
                    add_values[li, -L:, j] = arr[-L:]
            new_values = np.concatenate([base, add_values], axis=2)
            return HistoryPanel(values=new_values, levels=self.shares, rows=self.hdates, columns=new_cols)

        # 返回 DataFrame：MultiIndex 列 (share, output_name)
        data = {}
        for share, arrs in result_arrays.items():
            for name, arr in zip(out_names, arrs):
                data[(share, name)] = arr
        df = pd.DataFrame(data, index=idx)
        df.columns = pd.MultiIndex.from_tuples(df.columns, names=['share', 'output'])
        return df

    def candle_pattern(
            self,
            name: str,
            price_htypes: tuple[str, str, str, str] = ('open', 'high', 'low', 'close'),
            as_panel: bool = False,
            **kwargs,
    ):
        """基于 ta-lib 形态函数计算蜡烛形态信号。

        Parameters
        ----------
        name : str
            形态函数名称，如 'cdlhammer'。
        price_htypes : tuple of str, default ('open','high','low','close')
            OHLC 对应的 htypes 名称。
        as_panel : bool, default False
            False 返回 DataFrame（时间×股票），True 返回单一 htype 的 HistoryPanel。
        """
        if self.is_empty:
            return HistoryPanel() if as_panel else pd.DataFrame()
        import qteasy.tafuncs as tafuncs
        if not hasattr(tafuncs, name):
            raise ValueError(f'candle pattern function \"{name}\" not found in qteasy.tafuncs')
        func = getattr(tafuncs, name)

        o_name, h_name, l_name, c_name = price_htypes
        for nm in (o_name, h_name, l_name, c_name):
            if nm not in self.htypes:
                raise ValueError(f'price htype \"{nm}\" not found in HistoryPanel.htypes: {self.htypes}')

        oi = self.htypes.index(o_name)
        hi = self.htypes.index(h_name)
        li = self.htypes.index(l_name)
        ci = self.htypes.index(c_name)
        idx = pd.to_datetime(self.hdates)

        signals = np.zeros((self.level_count, self.row_count), dtype=float)
        for s_idx, share in enumerate(self.shares):
            o = pd.Series(self.values[s_idx, :, oi].astype(float), index=idx)
            h = pd.Series(self.values[s_idx, :, hi].astype(float), index=idx)
            l = pd.Series(self.values[s_idx, :, li].astype(float), index=idx)
            c = pd.Series(self.values[s_idx, :, ci].astype(float), index=idx)
            sig = func(o, h, l, c, **kwargs)
            sig_arr = np.asarray(sig, dtype=float).ravel()
            L = min(self.row_count, len(sig_arr))
            signals[s_idx, -L:] = sig_arr[-L:]

        if as_panel:
            return HistoryPanel(values=signals.reshape(self.level_count, self.row_count, 1),
                                levels=self.shares,
                                rows=self.hdates,
                                columns=[name])
        df = pd.DataFrame(signals.T, index=idx, columns=self.shares)
        return df

    def to_df_dict(self, by: str = 'share') -> dict:
        """ 将一个HistoryPanel转化为一个dict，这个dict的keys是HP中的shares，values是每个shares对应的历史数据
            这些数据以DataFrame的格式存储

        Parameters
        ----------
        by: str, {'share', 'shares', 'htype', 'htypes'}, Default: 'share'
            - 'share' 或 'shares': 将HistoryPanel中的数据切成若干片，每一片转化成一个DataFrame，
            它的keys是股票的代码，每个股票代码一个DataFrame
            - 'htype' 或 'htypes': 将HistoryPanel中的数据切成若干片，每一片转化成一个DataFrame，

            它的keys是历史数据类型，每种类型一个DataFrame

        Returns
        -------
        df_dict: dict, {str: pandas.DataFrame}

        Examples
        --------
        >>> hp = HistoryPanel(np.random.randn(2, 3, 4),
        ...                   rows=['2020-01-01', '2020-01-02', '2020-01-03'],
        ...                   levels=['000001', '000002', '000003'],
        ...                   columns=['close', 'open', 'high', 'low'])
        >>> hp
        share 0, label: 000001
                    close,  open,   high,   low
        2020-01-01  0.1,    0.2,    0.3,    0.4
        2020-01-02  0.5,    0.6,    0.7,    0.8
        2020-01-03  0.9,    1.0,    1.1,    1.2
        share 1, label: 000002
                    close,  open,   high,   low
        2020-01-01  1.1,    1.2,    1.3,    1.4
        2020-01-02  1.5,    1.6,    1.7,    1.8
        2020-01-03  1.9,    2.0,    2.1,    2.2
        share 2, label: 000003
                    close,  open,   high,   low
        2020-01-01  2.1,    2.2,    2.3,    2.4
        2020-01-02  2.5,    2.6,    2.7,    2.8
        2020-01-03  2.9,    3.0,    3.1,    3.2

        >>> hp.to_df_dict(by='share')
        {'000001':
                    close,  open,   high,   low
        2020-01-01  0.1,    0.2,    0.3,    0.4
        2020-01-02  0.5,    0.6,    0.7,    0.8
        2020-01-03  0.9,    1.0,    1.1,    1.2
        , '000002':
                    close,  open,   high,   low
        2020-01-01  1.1,    1.2,    1.3,    1.4
        2020-01-02  1.5,    1.6,    1.7,    1.8
        2020-01-03  1.9,    2.0,    2.1,    2.2
        , '000003':
                    close,  open,   high,   low
        2020-01-01  2.1,    2.2,    2.3,    2.4
        2020-01-02  2.5,    2.6,    2.7,    2.8
        2020-01-03  2.9,    3.0,    3.1,    3.2
        }

        >>> hp.to_df_dict(by='htype')
        {'close':
                    000001,  000002,  000003
        2020-01-01  0.1,     1.1,     2.1
        2020-01-02  0.5,     1.5,     2.5
        2020-01-03  0.9,     1.9,     2.9
        , 'open':
                    000001,  000002,  000003
        2020-01-01  0.2,     1.2,     2.2
        2020-01-02  0.6,     1.6,     2.6
        2020-01-03  1.0,     2.0,     3.0
        , 'high':
                    000001,  000002,  000003
        2020-01-01  0.3,     1.3,     2.3
        2020-01-02  0.7,     1.7,     2.7
        2020-01-03  1.1,     2.1,     3.1
        , 'low':
                    000001,  000002,  000003
        2020-01-01  0.4,     1.4,     2.4
        2020-01-02  0.8,     1.8,     2.8
        2020-01-03  1.2,     2.2,     3.2
        }

        """

        if not isinstance(by, str):
            raise TypeError(f'by ({by}) should be a string, and either "shares" or "htypes", got {type(by)}')
        assert by.lower() in ['share', 'shares', 'htype', 'htypes']

        df_dict = {}
        if self.is_empty:
            return df_dict

        if by.lower() in ['share', 'shares']:
            for share in self.shares:
                df_dict[share] = self.slice_to_dataframe(share=share)
            return df_dict

        if by.lower() in ['htype', 'htypes']:
            for htype in self.htypes:
                df_dict[htype] = self.slice_to_dataframe(htype=htype)
            return df_dict

    def unstack(self, by: str = 'share') -> dict:
        """ 等同于方法self.to_df_dict(), 是方法self.to_df_dict()的别称

        Parameters
        ----------
        by: str, {'share', 'htype'}, default 'share'
            指定按照share或者htype来unstack, 默认为share

        Returns
        -------
        dict
            unstack后的结果，是一个字典，key为share或htype，value为对应的DataFrame

        Examples
        --------
        >>> hp = HistoryPanel(np.random.randn(2, 3, 4),
        ...                   rows=['2020-01-01', '2020-01-02', '2020-01-03'],
        ...                   levels=['000001', '000002', '000003'],
        ...                   columns=['close', 'open', 'high', 'low'])
        >>> hp
        share 0, label: 000001
                    close,  open,   high,   low
        2020-01-01  0.1,    0.2,    0.3,    0.4
        2020-01-02  0.5,    0.6,    0.7,    0.8
        2020-01-03  0.9,    1.0,    1.1,    1.2
        share 1, label: 000002
                    close,  open,   high,   low
        2020-01-01  1.1,    1.2,    1.3,    1.4
        2020-01-02  1.5,    1.6,    1.7,    1.8
        2020-01-03  1.9,    2.0,    2.1,    2.2
        share 2, label: 000003
                    close,  open,   high,   low
        2020-01-01  2.1,    2.2,    2.3,    2.4
        2020-01-02  2.5,    2.6,    2.7,    2.8
        2020-01-03  2.9,    3.0,    3.1,    3.2

        >>> hp.unstack(by='share')
        {'000001':
                    close,  open,   high,   low
        2020-01-01  0.1,    0.2,    0.3,    0.4
        2020-01-02  0.5,    0.6,    0.7,    0.8
        2020-01-03  0.9,    1.0,    1.1,    1.2
        , '000002':
                    close,  open,   high,   low
        2020-01-01  1.1,    1.2,    1.3,    1.4
        2020-01-02  1.5,    1.6,    1.7,    1.8
        2020-01-03  1.9,    2.0,    2.1,    2.2
        , '000003':
                    close,  open,   high,   low
        2020-01-01  2.1,    2.2,    2.3,    2.4
        2020-01-02  2.5,    2.6,    2.7,    2.8
        2020-01-03  2.9,    3.0,    3.1,    3.2
        }

        >>> hp.unstack(by='htype')
        {'close':
                    000001,  000002,  000003
        2020-01-01  0.1,     1.1,     2.1
        2020-01-02  0.5,     1.5,     2.5
        2020-01-03  0.9,     1.9,     2.9
        , 'open':
                    000001,  000002,  000003
        2020-01-01  0.2,     1.2,     2.2
        2020-01-02  0.6,     1.6,     2.6
        2020-01-03  1.0,     2.0,     3.0
        , 'high':
                    000001,  000002,  000003
        2020-01-01  0.3,     1.3,     2.3
        2020-01-02  0.7,     1.7,     2.7
        2020-01-03  1.1,     2.1,     3.1
        , 'low':
                    000001,  000002,  000003
        2020-01-01  0.4,     1.4,     2.4
        2020-01-02  0.8,     1.8,     2.8
        2020-01-03  1.2,     2.2,     3.2
        }
        """

        return self.to_df_dict(by=by)

    def flattened_head(self, row_count=5):
        """ 以multi-index DataFrame的形式返回HistoryPanel的最初几行，默认五行

        Parameters
        ----------
        row_count: int, default 5
            打印的行数

        Returns
        -------
        dataframe, multi-indexed by share and htype as columns, with only first row_count rows
        一个dataframe，以share和htype为列的多重索引，只包含前row_count行

        Examples
        --------
        >>> data = np.array([[[12.3, 12.5, 1020010], [12.6, 13.2, 1020020], [12.9, 13.0, 1020030],
        ...                   [12.3, 12.5, 1020040], [12.6, 13.2, 1020050], [12.9, 13.0, 1020060]],
        ...                  [[2.3, 2.5, 20010], [2.6, 2.8, 20020], [2.9, 3.0, 20030],
        ...                   [2.3, 2.5, 20040], [2.6, 2.8, 20050], [2.9, 3.0, 20060]]])
        >>> hp = HistoryPanel(values=data,
        ...                          levels=['000300', '000001'],
        ...                          rows=pd.date_range('2020-01-01', periods=6),
        ...                          columns=['close', 'open', 'vol'])
        >>> hp
        share 0, label: 000300
                    close,  open,   vol
        2020-01-01  12.3,   12.5,   1020010
        2020-01-02  12.6,   13.2,   1020020
        2020-01-03  12.9,   13.0,   1020030
        2020-01-04  12.3,   12.5,   1020040
        2020-01-05  12.6,   13.2,   1020050
        2020-01-06  12.9,   13.0,   1020060
        share 1, label: 000001：
                    close,  open,   vol
        2020-01-01  2.3,    2.5,    20010
        2020-01-02  2.6,    3.2,    20020
        2020-01-03  2.9,    3.0,    20030
        2020-01-04  2.3,    2.5,    20040
        2020-01-05  2.6,    3.2,    20050
        2020-01-06  2.9,    3.0,    20060

        >>> hp.flattened_head(3)
                    000300                  000001
                    close,  open,   vol,    close,  open,   vol
        2020-01-01  12.3,   12.5,   1020010 2.3,    2.5,    20010
        2020-01-02  12.6,   13.2,   1020020 2.6,    3.2,    20020
        2020-01-03  12.9,   13.0,   1020030 2.9,    3.0,    20030
        """
        if row_count <= 0:
            raise ValueError("row_count should be positive")
        if row_count > self.shape[1]:
            row_count = self.shape[1]
        return self.flatten_to_dataframe(along='col').head(row_count)

    def flattened_tail(self, row_count=5):
        """ 以multi-index DataFrame的形式返回HistoryPanel的最后几行，默认五行

        Parameters
        ----------
        row_count: int, default 5
            打印的行数

        Returns
        -------
        dataframe, multi-indexed by share and htype as columns, with only last row_count rows
        一个dataframe，以share和htype为列的多重索引，只包含后row_count行

        Examples
        --------
        >>> data = np.array([[[12.3, 12.5, 1020010], [12.6, 13.2, 1020020], [12.9, 13.0, 1020030],
        ...                   [12.3, 12.5, 1020040], [12.6, 13.2, 1020050], [12.9, 13.0, 1020060]],
        ...                  [[2.3, 2.5, 20010], [2.6, 2.8, 20020], [2.9, 3.0, 20030],
        ...                   [2.3, 2.5, 20040], [2.6, 2.8, 20050], [2.9, 3.0, 20060]]])
        >>> hp = HistoryPanel(values=data,
        ...                          levels=['000300', '000001'],
        ...                          rows=pd.date_range('2020-01-01', periods=6),
        ...                          columns=['close', 'open', 'vol'])
        >>> hp
        share 0, label: 000300
                    close,  open,   vol
        2020-01-01  12.3,   12.5,   1020010
        2020-01-02  12.6,   13.2,   1020020
        2020-01-03  12.9,   13.0,   1020030
        2020-01-04  12.3,   12.5,   1020040
        2020-01-05  12.6,   13.2,   1020050
        2020-01-06  12.9,   13.0,   1020060
        share 1, label: 000001：
                    close,  open,   vol
        2020-01-01  2.3,    2.5,    20010
        2020-01-02  2.6,    3.2,    20020
        2020-01-03  2.9,    3.0,    20030
        2020-01-04  2.3,    2.5,    20040
        2020-01-05  2.6,    3.2,    20050
        2020-01-06  2.9,    3.0,    20060

        >>> hp.flattened_tail(3)
                    000300                  000001
                    close,  open,   vol,    close,  open,   vol
        2020-01-04  12.3,   12.5,   1020040 2.3,    2.5,    20040
        2020-01-05  12.6,   13.2,   1020050 2.6,    3.2,    20050
        2020-01-06  12.9,   13.0,   1020060 2.9,    3.0,    20060
        """
        if row_count <= 0:
            raise ValueError("row_count should be positive")
        if row_count > self.shape[1]:
            row_count = self.shape[1]
        return self.flatten_to_dataframe(along='col').tail(row_count)

    def head(self, row_count=5):
        """返回HistoryPanel的最初几行，默认五行

        Parameters
        ----------
        row_count: int, default 5
            打印的行数

        Returns
        -------
        dataframe, multi-indexed by share and htype as columns, with only first row_count rows
        一个dataframe，以share和htype为列的多重索引，只包含前row_count行

        Examples
        --------
        >>> data = np.array([[[12.3, 12.5, 1020010], [12.6, 13.2, 1020020], [12.9, 13.0, 1020030],
        ...                   [12.3, 12.5, 1020040], [12.6, 13.2, 1020050], [12.9, 13.0, 1020060]],
        ...                  [[2.3, 2.5, 20010], [2.6, 2.8, 20020], [2.9, 3.0, 20030],
        ...                   [2.3, 2.5, 20040], [2.6, 2.8, 20050], [2.9, 3.0, 20060]]])
        >>> hp = HistoryPanel(values=data,
        ...                          levels=['000300', '000001'],
        ...                          rows=pd.date_range('2020-01-01', periods=6),
        ...                          columns=['close', 'open', 'vol'])
        >>> hp
        share 0, label: 000300
                    close,  open,   vol
        2020-01-01  12.3,   12.5,   1020010
        2020-01-02  12.6,   13.2,   1020020
        2020-01-03  12.9,   13.0,   1020030
        2020-01-04  12.3,   12.5,   1020040
        2020-01-05  12.6,   13.2,   1020050
        2020-01-06  12.9,   13.0,   1020060
        share 1, label: 000001：
                    close,  open,   vol
        2020-01-01  2.3,    2.5,    20010
        2020-01-02  2.6,    3.2,    20020
        2020-01-03  2.9,    3.0,    20030
        2020-01-04  2.3,    2.5,    20040
        2020-01-05  2.6,    3.2,    20050
        2020-01-06  2.9,    3.0,    20060

        >>> hp.head(3)
        share 0, label: 000300
                    close,  open,   vol,
        2020-01-01  12.3,   12.5,   1020010
        2020-01-02  12.6,   13.2,   1020020
        2020-01-03  12.9,   13.0,   1020030
        share 1, label: 000001
                    close,  open,   vol
        2020-01-01  2.3,    2.5,    20010
        2020-01-02  2.6,    3.2,    20020
        2020-01-03  2.9,    3.0,    20030
        """
        if row_count <= 0:
            raise ValueError("row_count should be positive")
        if row_count > self.shape[1]:
            row_count = self.shape[1]
        return self.isegment(0, row_count)

    def tail(self, row_count=5):
        """返回HistoryPanel的最末几行，默认五行

        Parameters
        ----------
        row_count: int, default 5
            打印的行数

        Returns
        -------
        dataframe, multi-indexed by share and htype as columns, with only last row_count rows
        一个dataframe，以share和htype为列的多重索引，只包含最后row_count行

        Examples
        --------
        >>> data = np.array([[[12.3, 12.5, 1020010], [12.6, 13.2, 1020020], [12.9, 13.0, 1020030],
        ...                   [12.3, 12.5, 1020040], [12.6, 13.2, 1020050], [12.9, 13.0, 1020060]],
        ...                  [[2.3, 2.5, 20010], [2.6, 2.8, 20020], [2.9, 3.0, 20030],
        ...                   [2.3, 2.5, 20040], [2.6, 2.8, 20050], [2.9, 3.0, 20060]]])
        >>> hp = HistoryPanel(values=data,
        ...                          levels=['000300', '000001'],
        ...                          rows=pd.date_range('2020-01-01', periods=6),
        ...                          columns=['close', 'open', 'vol'])
        >>> hp
        share 0, label: 000300
                    close,  open,   vol
        2020-01-01  12.3,   12.5,   1020010
        2020-01-02  12.6,   13.2,   1020020
        2020-01-03  12.9,   13.0,   1020030
        2020-01-04  12.3,   12.5,   1020040
        2020-01-05  12.6,   13.2,   1020050
        2020-01-06  12.9,   13.0,   1020060
        share 1, label: 000001：
                    close,  open,   vol
        2020-01-01  2.3,    2.5,    20010
        2020-01-02  2.6,    3.2,    20020
        2020-01-03  2.9,    3.0,    20030
        2020-01-04  2.3,    2.5,    20040
        2020-01-05  2.6,    3.2,    20050
        2020-01-06  2.9,    3.0,    20060

        >>> hp.tail(3)
        share 0, label: 000300
                    close,  open,   vol
        2020-01-04  12.3,   12.5,   1020040
        2020-01-05  12.6,   13.2,   1020050
        2020-01-06  12.9,   13.0,   1020060
        share 1, label: 000001：
                    close,  open,   vol
        2020-01-04  2.3,    2.5,    20040
        2020-01-05  2.6,    3.2,    20050
        2020-01-06  2.9,    3.0,    20060
        """
        if row_count <= 0:
            raise ValueError("row_count should be positive")
        if row_count > self.shape[1]:
            row_count = self.shape[1]
        return self.isegment(- row_count, None)

    def plot(
        self,
        shares: Optional[Union[str, Iterable[str]]] = None,
        layout: str = 'auto',
        interactive: bool = False,
        highlight: Optional[Any] = None,
        **kwargs,
    ):
        """根据 HistoryPanel 中已有的 htypes 与 shares 自动选择图表类型并绘制图表。

        本方法只消费已有数据不做新增计算，图表类型由内部注册表基于 htypes 决定（如
        OHLC→K 线，vol→成交量，MACD 三列→MACD 图，其余→折线），支持单标的与多标
        的 overlay/stack 布局，以及基于 matplotlib 的静态图和基于 Plotly 的交互式图表。

        Parameters
        ----------
        shares : str or sequence of str, optional
            要参与绘图的标的子集；默认使用 HistoryPanel 的全部 shares。
        layout : {'overlay', 'stack', 'auto'}, default 'auto'
            多标的布局方式；'overlay' 为同组叠加，'stack' 为多组分行展示，'auto' 时
            2 只标的用 overlay，3 只及以上用 stack。
        interactive : bool, default False
            为 True 时使用 Plotly 交互后端（需安装 plotly 及 anywidget/ipywidgets）；
            为 False 时使用 matplotlib 静态后端。
        highlight : dict or str, optional
            高亮配置，可为 ``{'condition': 'max'|'min' 或布尔数组, 'style': {...}}``，
            或简写为 'max' / 'min'。
        **kwargs
            预留的扩展参数，当前版本中不使用。

        Returns
        -------
        matplotlib.figure.Figure or plotly.graph_objs.FigureWidget or _PlotlyFigureWrapper
            interactive=False 时返回 matplotlib Figure；
            interactive=True 且在 Jupyter 中返回 FigureWidget（支持表头信息与 Y 轴自适应）；
            interactive=True 且非 Jupyter 环境时返回内嵌 HTML 包装器。

        Examples
        --------
        >>> import qteasy as qt
        >>> hp = qt.get_history_data(htype_names='open, high, low, close, vol',
        ...                          shares='000300.SH', rows=200)
        >>> fig = hp.plot()

        >>> fig_interactive = hp.plot(interactive=True, highlight='max')

        See Also
        --------
        qt.get_kline
            获取 K 线数据并可选 ``as_panel=True`` 得到 HistoryPanel。
        """
        if self.is_empty:
            raise ValueError('Cannot plot an empty HistoryPanel')
        from qteasy.hp_visual_spec import (
            get_chart_type_registry,
            build_kline_spec,
            build_volume_spec,
            build_macd_spec,
            build_line_spec,
        )
        from qteasy.hp_visual_render import build_figure_from_specs
        share_list = list(self.shares) if shares is None else (
            [shares] if isinstance(shares, str) else list(shares)
        )
        share_list = [s for s in share_list if s in self.shares]
        if not share_list:
            share_list = list(self.shares)
        if len(share_list) > 5:
            try:
                from qteasy import logger_core
                logger_core.warning(
                    f'HistoryPanel.plot: more than 5 shares requested ({len(share_list)}), '
                    'only the first 5 will be displayed.'
                )
            except Exception:
                import warnings
                warnings.warn(
                    f'HistoryPanel.plot: more than 5 shares requested ({len(share_list)}), '
                    'only the first 5 will be displayed.',
                    UserWarning,
                    stacklevel=2,
                )
            share_list = share_list[:5]
        registry = get_chart_type_registry()
        types_info = registry.get_applicable_types(self.htypes)
        if not types_info:
            from qteasy.hp_visual_render import _HAS_MPL
            if _HAS_MPL:
                import matplotlib.pyplot as plt
                fig, _ = plt.subplots(1, 1, figsize=(8, 4))
                return fig
            raise RuntimeError('No applicable chart types and matplotlib not available')
        n_share = len(share_list)
        if layout == 'auto':
            layout = 'overlay' if n_share == 2 else 'stack'
        if n_share == 1:
            groups = [share_list]
        elif layout == 'overlay' and n_share == 2:
            groups = [share_list]
        else:
            groups = [[s] for s in share_list]
        x_dates = list(self.hdates)
        specs_per_group = []
        group_titles = []
        for grp in groups:
            row = []
            for info in types_info:
                tid = info.id
                if tid == 'kline':
                    spec = build_kline_spec(self, shares=grp)
                elif tid == 'volume':
                    spec = build_volume_spec(self, shares=grp)
                elif tid == 'macd':
                    spec = build_macd_spec(self, shares=grp)
                elif tid == 'line':
                    spec = build_line_spec(self, shares=grp)
                else:
                    spec = None
                if spec is not None and highlight is not None:
                    spec = dict(spec)
                    spec['highlight'] = (
                        highlight if isinstance(highlight, dict) else {'condition': highlight}
                    )
                row.append(spec)
            kline_idx = next((i for i, t in enumerate(types_info) if t.id == 'kline'), None)
            vol_idx = next((i for i, t in enumerate(types_info) if t.id == 'volume'), None)
            if (
                kline_idx is not None and vol_idx is not None
                and row[kline_idx] is not None and row[vol_idx] is not None
                and 'open' in row[kline_idx].get('data', {}) and 'close' in row[kline_idx].get('data', {})
            ):
                vol_spec = dict(row[vol_idx])
                vol_spec['data'] = dict(vol_spec.get('data', {}))
                vol_spec['data']['open'] = row[kline_idx]['data']['open']
                vol_spec['data']['close'] = row[kline_idx]['data']['close']
                row[vol_idx] = vol_spec
            specs_per_group.append(row)
            group_titles.append(grp[0] if len(grp) == 1 else ','.join(grp[:3]))
        if interactive:
            from qteasy.hp_visual_plotly import (
                _HAS_PLOTLY,
                build_interactive_figure_from_specs,
                _wrap_figure_for_notebook,
                _can_use_figure_widget,
                _create_figure_widget_with_callbacks,
            )
            if not _HAS_PLOTLY:
                raise RuntimeError(
                    'interactive=True requires plotly. Install with: pip install plotly'
                )
            fig = build_interactive_figure_from_specs(
                specs_per_group,
                types_info,
                x_dates=x_dates,
                group_titles=group_titles,
            )
            if _can_use_figure_widget():
                return _create_figure_widget_with_callbacks(fig)
            return _wrap_figure_for_notebook(fig)
        fig = build_figure_from_specs(
            specs_per_group,
            types_info,
            x_dates=x_dates,
            group_titles=group_titles,
        )
        return fig

    # 以下 legacy 方法仅保留占位，统一通过 HistoryPanel.plot() 实现可视化
    def candle(self, *args, **kwargs):
        """基于当前 ``HistoryPanel`` 数据绘制蜡烛图（已由 ``plot()`` 统一处理）

        Notes
        -----
        - 新版可视化推荐直接调用 ``HistoryPanel.plot()``，并通过 htypes / layout
          控制是否输出 K 线、成交量等图表类型。
        - 本方法在内部会委托给可视化子模块的统一入口实现，行为与 ``plot()`` 保持
          一致，仅作为语义化别名存在。
        """
        raise NotImplementedError

    def ohlc(self, *args, **kwargs):
        """ plot ohlc chart with data in the HistoryPanel, check data availability before plotting

        args:
        kwargs:
        :return:
        """
        raise NotImplementedError

    # TODO: implement this method
    def renko(self, *args, **kwargs):
        """ plot renko chart with data in the HistoryPanel, check data availability before plotting

        args:
        kwargs:
        :return:
        """
        raise NotImplementedError


class _HistoryPanelKlineAccessor:
    """HistoryPanel 的 K 线技术指标访问器，内部使用，通过 HistoryPanel.kline 访问。"""

    def __init__(self, hp: HistoryPanel):
        self._hp = hp

    def _get_price(self, price_htype: str):
        """取指定 htype 的价格矩阵 (shares, times)。"""
        if self._hp.is_empty or price_htype not in self._hp.htypes:
            raise ValueError(f'price_htype "{price_htype}" not in htypes: {self._hp.htypes}')
        ci = self._hp.htypes.index(price_htype)
        return self._hp.values[:, :, ci].astype(float)

    def _append_htypes(self, new_columns: list, new_arrays: list) -> HistoryPanel:
        """在原有 Panel 后追加新 htype 列，new_arrays 为 list of (n_share, n_time) 数组。"""
        hp = self._hp
        if hp.is_empty:
            return HistoryPanel()
        base = hp.values.astype(float)
        to_add = np.stack(new_arrays, axis=2)  # (L, R, C_new)
        new_values = np.concatenate([base, to_add], axis=2)
        new_htypes = list(hp.htypes) + list(new_columns)
        return HistoryPanel(values=new_values, levels=hp.shares, rows=hp.hdates, columns=new_htypes)

    def sma(self, window: int = 20, price_htype: str = 'close', new_htype: Optional[str] = None) -> HistoryPanel:
        """简单移动平均。"""
        from qteasy import tafuncs
        default_name = f'sma_{window}'
        if new_htype is None:
            new_htype = default_name
        if new_htype in self._hp.htypes:
            raise ValueError(f'new_htype "{new_htype}" already exists in htypes')
        prices = self._get_price(price_htype)
        n_share, n_time = prices.shape
        out = np.full_like(prices, np.nan, dtype=float)
        for i in range(n_share):
            out[i, :] = tafuncs.sma(prices[i, :], timeperiod=window)
        return self._append_htypes([new_htype], [out])

    def ema(self, span: int = 20, price_htype: str = 'close', new_htype: Optional[str] = None) -> HistoryPanel:
        """指数移动平均。"""
        from qteasy import tafuncs
        default_name = f'ema_{span}'
        if new_htype is None:
            new_htype = default_name
        if new_htype in self._hp.htypes:
            raise ValueError(f'new_htype "{new_htype}" already exists in htypes')
        prices = self._get_price(price_htype)
        n_share, n_time = prices.shape
        out = np.full_like(prices, np.nan, dtype=float)
        for i in range(n_share):
            res = tafuncs.ema(prices[i, :], span=span)
            arr = np.atleast_1d(np.asarray(res, dtype=float)).ravel()
            out[i, :min(n_time, len(arr))] = arr[:n_time]
        return self._append_htypes([new_htype], [out])

    def bbands(
            self,
            window: int = 20,
            price_htype: str = 'close',
            nbdev_up: float = 2.0,
            nbdev_dn: float = 2.0,
            ma_type: str = 'sma',
            suffix: Optional[str] = None,
    ) -> HistoryPanel:
        """布林带。"""
        from qteasy import tafuncs
        tag = suffix if suffix is not None else f'{window}_{int(nbdev_up)}_{int(nbdev_dn)}'
        upper_name = f'bbands_upper_{tag}'
        middle_name = f'bbands_middle_{tag}'
        lower_name = f'bbands_lower_{tag}'
        for n in (upper_name, middle_name, lower_name):
            if n in self._hp.htypes:
                raise ValueError(f'htype "{n}" already exists')
        prices = self._get_price(price_htype)
        n_share, n_time = prices.shape
        matype = 0 if ma_type == 'sma' else 1  # tafuncs 使用 matype 整数
        u = np.full_like(prices, np.nan, dtype=float)
        m = np.full_like(prices, np.nan, dtype=float)
        l = np.full_like(prices, np.nan, dtype=float)
        for i in range(n_share):
            uu, mm, ll = tafuncs.bbands(
                prices[i, :], timeperiod=window,
                nbdevup=int(nbdev_up), nbdevdn=int(nbdev_dn), matype=matype,
            )
            uu = np.asarray(uu).ravel()
            mm = np.asarray(mm).ravel()
            ll = np.asarray(ll).ravel()
            L = min(n_time, len(uu), len(mm), len(ll))
            u[i, -L:] = uu[-L:]
            m[i, -L:] = mm[-L:]
            l[i, -L:] = ll[-L:]
        return self._append_htypes([upper_name, middle_name, lower_name], [u, m, l])

    def macd(
            self,
            price_htype: str = 'close',
            fastperiod: int = 12,
            slowperiod: int = 26,
            signalperiod: int = 9,
            suffix: Optional[str] = None,
    ) -> HistoryPanel:
        """MACD 指标。"""
        from qteasy import tafuncs
        tag = suffix if suffix is not None else f'{fastperiod}_{slowperiod}_{signalperiod}'
        n1, n2, n3 = f'macd_{tag}', f'macd_signal_{tag}', f'macd_hist_{tag}'
        for n in (n1, n2, n3):
            if n in self._hp.htypes:
                raise ValueError(f'htype "{n}" already exists')
        prices = self._get_price(price_htype)
        n_share, n_time = prices.shape
        macd_arr = np.full_like(prices, np.nan, dtype=float)
        sig_arr = np.full_like(prices, np.nan, dtype=float)
        hist_arr = np.full_like(prices, np.nan, dtype=float)
        for i in range(n_share):
            mc, sig, hist = tafuncs.macd(prices[i, :], fastperiod=fastperiod, slowperiod=slowperiod, signalperiod=signalperiod)
            mc, sig, hist = np.asarray(mc).ravel(), np.asarray(sig).ravel(), np.asarray(hist).ravel()
            L = min(n_time, len(mc), len(sig), len(hist))
            macd_arr[i, -L:] = mc[-L:]
            sig_arr[i, -L:] = sig[-L:]
            hist_arr[i, -L:] = hist[-L:]
        return self._append_htypes([n1, n2, n3], [macd_arr, sig_arr, hist_arr])

    def kdj(
            self,
            price_htype: str = 'close',
            fastk_period: int = 9,
            slowk_period: int = 3,
            slowd_period: int = 3,
            suffix: Optional[str] = None,
    ) -> HistoryPanel:
        """KDJ 随机指标，需要 high、low、close。"""
        from qteasy import tafuncs
        for h in ('high', 'low', 'close'):
            if h not in self._hp.htypes:
                raise ValueError(f'KDJ requires high/low/close in htypes, missing "{h}"')
        tag = suffix if suffix is not None else f'{fastk_period}_{slowk_period}_{slowd_period}'
        k_name = f'kdj_k_{tag}'
        d_name = f'kdj_d_{tag}'
        j_name = f'kdj_j_{tag}'
        for n in (k_name, d_name, j_name):
            if n in self._hp.htypes:
                raise ValueError(f'htype "{n}" already exists')
        high = self._get_price('high')
        low = self._get_price('low')
        close = self._get_price('close')
        n_share, n_time = high.shape
        k_arr = np.full_like(close, np.nan, dtype=float)
        d_arr = np.full_like(close, np.nan, dtype=float)
        j_arr = np.full_like(close, np.nan, dtype=float)
        for i in range(n_share):
            kk, dd = tafuncs.stoch(high[i, :], low[i, :], close[i, :],
                                   fastk_period=fastk_period, slowk_period=slowk_period, slowd_period=slowd_period)
            kk = np.asarray(kk).ravel()[:n_time]
            dd = np.asarray(dd).ravel()[:n_time]
            jj = 3 * kk - 2 * dd  # J = 3*K - 2*D
            k_arr[i, :] = kk
            d_arr[i, :] = dd
            j_arr[i, :] = jj
        return self._append_htypes([k_name, d_name, j_name], [k_arr, d_arr, j_arr])


    def apply_ta(
            self,
            func_name: str,
            htype: str = 'close',
            shares: Optional[Iterable[str]] = None,
            as_panel: bool = True,
            **kwargs,
    ):
        """兼容旧计划的命名，为 kline 访问器预留统一 ta 接口（实际实现位于 HistoryPanel.apply_ta）。"""
        return self._hp.apply_ta(func_name=func_name, htype=htype, shares=shares, as_panel=as_panel, **kwargs)


class HistoryPanelRolling:
    """HistoryPanel 的滚动窗口统计对象。

    该对象通常由 :meth:`HistoryPanel.rolling` 创建，对应一个固定的窗口
    参数组合，并提供 ``mean/std/sum/min/max/apply`` 等方法，返回新的
    HistoryPanel。
    """

    def __init__(self,
                 hp: HistoryPanel,
                 window: int,
                 min_periods: int,
                 center: bool,
                 by: str):
        self._hp = hp
        self._window = window
        self._min_periods = min_periods
        self._center = center
        self._by = by

    def _apply_rolling(self, func_name: str) -> HistoryPanel:
        """内部通用滚动聚合实现。"""
        hp = self._hp
        if hp.is_empty:
            return HistoryPanel()
        values = hp.values.astype(float)
        res = np.full_like(values, np.nan, dtype=float)

        l_cnt, r_cnt, c_cnt = values.shape

        if self._by == 'share':
            # 每只股票、每个 htype 独立做时间滚动
            for li in range(l_cnt):
                for ci in range(c_cnt):
                    series = pd.Series(values[li, :, ci], index=hp.hdates)
                    roller = series.rolling(window=self._window,
                                            min_periods=self._min_periods,
                                            center=self._center)
                    rolled = getattr(roller, func_name)()
                    res[li, :, ci] = rolled.values
        else:  # by == 'htype'
            for ci in range(c_cnt):
                for li in range(l_cnt):
                    series = pd.Series(values[li, :, ci], index=hp.hdates)
                    roller = series.rolling(window=self._window,
                                            min_periods=self._min_periods,
                                            center=self._center)
                    rolled = getattr(roller, func_name)()
                    res[li, :, ci] = rolled.values

        return HistoryPanel(values=res, levels=hp.shares, rows=hp.hdates, columns=hp.htypes)

    def mean(self) -> HistoryPanel:
        """滚动窗口均值。"""
        return self._apply_rolling('mean')

    def std(self) -> HistoryPanel:
        """滚动窗口标准差。"""
        return self._apply_rolling('std')

    def sum(self) -> HistoryPanel:
        """滚动窗口求和。"""
        return self._apply_rolling('sum')

    def min(self) -> HistoryPanel:
        """滚动窗口最小值。"""
        return self._apply_rolling('min')

    def max(self) -> HistoryPanel:
        """滚动窗口最大值。"""
        return self._apply_rolling('max')

    def apply(self,
              func: Callable[[np.ndarray], float],
              raw: bool = False,
              **kwargs) -> HistoryPanel:
        """在滚动窗口上应用自定义函数。

        Parameters
        ----------
        func : callable
            自定义函数，接受一个窗口向量并返回标量。
        raw : bool, default False
            为 ``True`` 时向 func 传入 ``ndarray``，否则传入 ``Series``。
        **kwargs :
            透传给 func 的其他参数。
        """
        hp = self._hp
        if hp.is_empty:
            return HistoryPanel()
        values = hp.values.astype(float)
        res = np.full_like(values, np.nan, dtype=float)
        l_cnt, r_cnt, c_cnt = values.shape

        def _apply_series(s: pd.Series) -> float:
            if raw:
                return func(s.values, **kwargs)
            return func(s, **kwargs)

        if self._by == 'share':
            for li in range(l_cnt):
                for ci in range(c_cnt):
                    series = pd.Series(values[li, :, ci], index=hp.hdates)
                    roller = series.rolling(window=self._window,
                                            min_periods=self._min_periods,
                                            center=self._center)
                    rolled = roller.apply(_apply_series, raw=False)
                    res[li, :, ci] = rolled.values
        else:
            for ci in range(c_cnt):
                for li in range(l_cnt):
                    series = pd.Series(values[li, :, ci], index=hp.hdates)
                    roller = series.rolling(window=self._window,
                                            min_periods=self._min_periods,
                                            center=self._center)
                    rolled = roller.apply(_apply_series, raw=False)
                    res[li, :, ci] = rolled.values

        return HistoryPanel(values=res, levels=hp.shares, rows=hp.hdates, columns=hp.htypes)


def hp_join(*historypanels):
    """ 当元组*historypanels不是None，且内容全都是HistoryPanel对象时，将所有的HistoryPanel对象连接成一个HistoryPanel

    parameters
    ----------
    historypanels: HistoryPanels
        一个或多个HistoryPanel对象，他们将被组合成一个包含他们所有数据的HistoryPanel

    Returns
    -------
    HistoryPanel
        组合后的HistoryPanel
    """
    assert all(isinstance(hp, HistoryPanel) for hp in historypanels), \
        f'Object type Error, all objects passed to this function should be HistoryPanel'
    res_hp = HistoryPanel()
    for hp in historypanels:
        if isinstance(hp, HistoryPanel):
            res_hp.join(other=hp)
    return res_hp


def dataframe_to_hp(
        df: pd.DataFrame,
        hdates=None,
        htypes=None,
        shares=None,
        column_type: str = None
):
    """ 根据DataFrame中的数据创建HistoryPanel对象。

    Parameters
    ----------
    df: pd.DataFrame,
        需要被转化为HistoryPanel的DataFrame。
    hdates: DatetimeIndex or List of DateTime like, Optional
        如果给出hdates，它会被用于转化后HistoryPanel的日期标签
    htypes: str or list of str, Optional
        转化后HistoryPanel的历史数据类型标签
    shares: str or list of str, Optional
        转化后HistoryPanel的股票代码标签
    column_type: str, Default None
        DataFrame的column代表的数据类型，可以为 'shares' or 'htype'
        如果为None，则必须输入htypes和shares参数中的一个

    Returns
    -------
    HistoryPanel对象

    Notes
    -----
    由于DataFrame只有一个二维数组，因此一个DataFrame只能转化为以下两种HistoryPanel之一：
    1，只有一个share，包含一个或多个htype的HistoryPanel，这时HistoryPanel的shape为(1, dates, htypes)
        在这种情况下，htypes可以由一个列表，或逗号分隔字符串给出，也可以由DataFrame对象的column Name来生成，而share则必须给出
    2，只有一个dtype，包含一个或多个shares的HistoryPanel，这时HistoryPanel的shape为(shares, dates, 1)
    具体转化为何种类型的HistoryPanel可以由column_type参数来指定，也可以通过给出hdates、htypes以及shares参数来由程序判断

    Examples
    --------
    >>> dataframe = pd.DataFrame(
    ...     data=np.random.rand(3, 3),
    ...     index=pd.date_range(start='2020-01-01', periods=3),
    ...     columns=['A', 'B', 'C']
    ... )
    >>> dataframe
    Out:
                       A         B         C
    2020-01-01  0.814394  0.284772  0.259304
    2020-01-02  0.237300  0.483317  0.600886
    2020-01-03  0.744638  0.255470  0.953640

    >>> hp = dataframe_to_hp(dataframe, htypes=['open', 'close', 'high'], shares='000001')
    >>> hp
    share 0, label: 000001
                    open     close      high
    2020-01-01  0.814394  0.284772  0.259304
    2020-01-02  0.237300  0.483317  0.600886
    2020-01-03  0.744638  0.255470  0.953640

    >>> hp = dataframe_to_hp(dataframe, htypes='open', shares=['000001', '000002', '000003'])
    >>> hp
    share 0, label: 000001
                    open
    2020-01-01  0.814394
    2020-01-02  0.237300
    2020-01-03  0.744638
    share 1, label: 000002
                    open
    2020-01-01  0.284772
    2020-01-02  0.483317
    2020-01-03  0.255470
    share 2, label: 000003
                    open
    2020-01-01  0.259304
    2020-01-02  0.600886
    2020-01-03  0.953640
    """

    available_column_types = ['shares', 'htypes', None]
    if not isinstance(df, pd.DataFrame):
        msg = f'Input df should be pandas DataFrame! got {type(df)} instead.'
        raise TypeError(msg)
    if hdates is None:
        hdates = df.rename(index=pd.to_datetime).index
    # if not isinstance(hdates, (list, tuple)):
    #     msg = f'TypeError, hdates should be list or tuple, got {type(hdates)} instead.'
    #     raise TypeError(msg)
    index_count = len(hdates)
    if not index_count == len(df.index):
        msg = f'InputError, can not match {index_count} indices with {len(df.hdates)} rows of DataFrame'
        raise ValueError(msg)
    if not column_type in available_column_types:
        msg = f'column_type should be either "shares" or "htypes", got {type(column_type)} instead!'
        raise ValueError(msg)
    # TODO: Temp codes, implement this method when column_type is not given -- the column type should be infered
    #  by the input combination of shares and htypes
    if column_type is None:  # try to infer a proper column type
        if shares is None:
            htype_list = []
            if htypes is None:
                raise KeyError(f'shares and htypes can not both be None if column_type is not given!')
            if isinstance(htypes, str):
                htype_list = str_to_list(htypes)
            try:
                if len(htype_list) == 1:
                    column_type = 'shares'
                else:
                    column_type = 'htypes'
            except:
                raise ValueError(f'htypes should be a list or a string, got {type(htypes)} instead')
        else:
            share_list = []
            if shares is None:
                raise KeyError(f'shares and htypes can not both be None if column_type is not given!')
            if isinstance(shares, str):
                share_list = str_to_list(shares)
            try:
                if len(share_list) == 1:
                    column_type = 'htypes'
                else:
                    column_type = 'shares'
            except:
                raise ValueError(f'shares should be a list or a string, got {type(shares)} instead')

    if column_type == 'shares':
        if shares is None:
            shares = df.columns
        if isinstance(shares, str):
            shares = str_to_list(shares, sep_char=',')
        if not len(shares) == len(df.columns):
            msg = f'Can not match {len(shares)} shares with {len(df.columns)} columns of DataFrame'
            raise ValueError(msg)
        if htypes is None:
            raise KeyError(f', Please provide a valid name for the htype of the History Panel')
        assert isinstance(htypes, str), \
            f'TypeError, data type of dtype should be a string, got {type(htypes)} instead.'
        share_count = len(shares)
        history_panel_value = np.zeros(shape=(share_count, len(hdates), 1))
        for i in range(share_count):
            history_panel_value[i, :, 0] = df.values[:, i]
    else:  # column_type == 'htype'
        if htypes is None:
            htypes = df.columns
        elif isinstance(htypes, str):
            htypes = htypes.split(',')
            assert len(htypes) == len(df.columns), \
                f'InputError, can not match {len(htypes)} shares with {len(df.columns)} columns of DataFrame'
        else:
            assert isinstance(htypes, (list, tuple)), f'TypeError: levels should be list or tuple, ' \
                                                      f'got {type(htypes)} instead.'
            assert len(htypes) == len(df.columns), \
                f'InputError, can not match {len(htypes)} shares with {len(df.columns)} columns of DataFrame'
        assert shares is not None, f'InputError, shares should be given when they can not inferred'
        assert isinstance(shares, str), \
            f'TypeError, data type of share should be a string, got {type(shares)} instead.'
        history_panel_value = df.values.reshape(1, len(hdates), len(htypes))
    return HistoryPanel(values=history_panel_value, levels=shares, rows=hdates, columns=htypes)


def from_single_dataframe(df: pd.DataFrame,
                          hdates=None,
                          htypes=None,
                          shares=None,
                          column_type: str = None) -> HistoryPanel:
    """ 函数dataframe_to_hp()的别称，等同于dataframe_to_hp()"""
    return dataframe_to_hp(df=df,
                           hdates=hdates,
                           htypes=htypes,
                           shares=shares,
                           column_type=column_type)


def from_multi_index_dataframe(df: pd.DataFrame):
    """ 将一个含有multi-index的DataFrame转化为一个HistoryPanel

    Parameters
    ----------
    df: pd.DataFrame
        需要被转化的DataFrame

    Returns
    -------
    HistoryPanel

    """
    raise NotImplementedError


def stack_dataframes(dfs: Union[list, dict],
                     dataframe_as: str = 'shares',
                     shares: Iterable = None,
                     htypes: Iterable = None,
                     fill_value: Any = None):
    """ 将多个dataframe组合成一个HistoryPanel.

    Parameters
    ----------
    dfs: list of DataFrames or dict of DataFrames
        需要被堆叠的dataframe，可以为list或dict，
        dfs可以是一个dict或一个list，如果是一个list，这个list包含需要组合的所有dataframe，如果是dict，这个dict的values包含
        所有需要组合的dataframe，dict的key包含每一个dataframe的标签，这个标签可以被用作HistoryPanel的层（shares）或列
        （htypes）标签。如果dfs是一个list，则组合后的行标签或列标签必须明确给出。
    dataframe_as: str {'shares', 'htypes'}
        每个dataframe代表的数据类型。
        dataframe_as == 'shares'，
            表示每个DataFrame代表一个share的数据，每一列代表一个htype。组合后的HP对象
            层数与DataFrame的数量相同，而列数等于所有DataFrame的列的并集，行标签也为所有DataFrame的行标签的并集
            在这种模式下：
            如果dfs是一个list，shares参数必须给出，且shares的数量必须与DataFrame的数量相同，作为HP的层标签
            如果dfs是一个dict，shares参数不必给出，dfs的keys会被用于层标签，如果shares参数给出且符合要求，
            shares参数将取代dfs的keys参数

        dataframe_as == 'htypes'，
            表示每个DataFrame代表一个htype的数据，每一列代表一个share。组合后的HP对象
            列数与DataFrame的数量相同，而层数等于所有DataFrame的列的并集，行标签也为所有DataFrame的行标签的并集
            在这种模式下，
            如果dfs是一个list，htypes参数必须给出，且htypes的数量必须与DataFrame的数量相同，作为HP的列标签
            如果dfs是一个dict，htypes参数不必给出，dfs的keys会被用于列标签，如果htypes参数给出且符合要求，
            htypes参数将取代dfs的keys参数
    shares: str or list of str
        生成的HistoryPanel的层标签或股票名称标签。
        如果堆叠方式为"shares"，则层标签必须以dict的key的形式给出或者在shares参数中给出
        以下两种参数均有效且等效：
        '000001.SZ, 000002.SZ, 000003.SZ'
        ['000001.SZ', '000002.SZ', '000003.SZ']

        如果堆叠方式为"htypes"，不需要给出shares，默认使用dfs的columns标签的并集作为输出的层标签
        如果给出了shares，则会强制使用shares作为层标签，多出的标签会用fill_values填充，
        多余的DataFrame数据会被丢弃
    htypes: str or list of str
        生成的HistoryPanel的列标签或数据类型标签。
        如果堆叠方式为"htypes"，则层标签必须以dict的key的形式给出或者在shares参数中给出
        以下两种参数均有效且等效：
        '000001.SZ, 000002.SZ, 000003.SZ'
        ['000001.SZ', '000002.SZ', '000003.SZ']
        如果堆叠方式为"shares"，不需要给出htypes，默认使用dfs的columns标签的并集作为列标签
        如果给出了htypes，则会强制用它作为列标签，多出的标签会用fill_values填充，
        多余的DataFrame数据会被丢弃
    fill_value:
        多余的位置用fill_value填充

    Returns
    -------
    HistoryPanel
        一个由多个单index的数据框组成的HistoryPanel对象

    Examples
    --------
    >>> df1 = pd.DataFrame([[1, 2, 3], [4, 5, 6]], index=['20210101', '20210102'], columns=['open', 'close', 'low'])
    >>> df2 = pd.DataFrame([[7, 8, 9], [10, 11, 12]], index=['20210101', '20210102'], columns=['open', 'close', 'low'])
    >>> df3 = pd.DataFrame([[13, 14, 15], [16, 17, 18]], index=['20210101', '20210102'], columns=['open', 'close', 'low'])
    >>> dataframes = [df1, df2, df3]
    >>> hp = stack_dataframes(dataframes, dataframe_as='shares', shares='000001.SZ, 000002.SZ, 000003.SZ')
    >>> hp
    share 0, label: 000001.SZ
             open  close   low
    20210101  1.0    2.0   3.0
    20210102  4.0    5.0   6.0
    share 1, label: 000002.SZ
              open  close   low
    20210101   7.0    8.0   9.0
    20210102  10.0   11.0  12.0
    share 2, label: 000003.SZ
              open  close   low
    20210101  13.0   14.0  15.0
    20210102  16.0   17.0  18.0

    """
    assert isinstance(dfs, (list, dict)), \
        f'TypeError, dfs should be a list of or a dict whose values are pandas DataFrames, got {type(dfs)} instead.'
    assert dataframe_as in ['shares', 'htypes'], \
        f'InputError, valid input for dataframe_as can only be \'shares\' or \'htypes\''
    if fill_value is None:
        fill_value = np.nan
    assert isinstance(fill_value, (int, float)), f'invalid fill value type {type(fill_value)}'
    if shares is not None:
        if isinstance(shares, str):
            shares = str_to_list(shares)
    if htypes is not None:
        if isinstance(htypes, str):
            htypes = str_to_list(htypes)
    combined_index = []
    combined_shares = []
    combined_htypes = []
    # 检查输入参数是否正确
    if dataframe_as == 'shares':
        axis_names = shares
        combined_axis_names = combined_shares
    else:  # dataframe_as == 'htypes':
        axis_names = htypes
        combined_axis_names = combined_htypes
    # 根据叠放方式不同，需要检查的参数也不同
    assert (axis_names is not None) or (isinstance(dfs, dict)), \
        f'htypes should be given if the dataframes are to be stacked along htypes and they are not in a dict'
    assert isinstance(axis_names, (list, str)) or (isinstance(dfs, dict))
    if isinstance(axis_names, str):
        axis_names = str_to_list(axis_names)
    if isinstance(dfs, dict) and axis_names is None:
        axis_names = dfs.keys()
    assert len(axis_names) == len(dfs)
    combined_axis_names.extend(axis_names)

    if isinstance(dfs, dict):
        dfs = dfs.values()
    # 逐个处理所有传入的DataFram，合并index、htypes以及shares
    for df in dfs:
        assert isinstance(df, pd.DataFrame), \
            f'InputError, dfs should be a list of pandas DataFrame, got {type(df)} instead.'
        combined_index.extend(df.rename(index=pd.to_datetime).index)
        if dataframe_as == 'shares':
            combined_htypes.extend(df.columns)
        else:
            combined_shares.extend(df.columns)
    dfs = [df.rename(index=pd.to_datetime) for df in dfs]
    # 合并htypes及shares，
    # 如果没有直接给出shares或htypes，使用他们的并集并排序
    # 如果直接给出了shares或htypes，直接使用并保持原始顺序
    if (dataframe_as == 'shares') and (htypes is None):
        combined_htypes = list(set(combined_htypes))
        combined_htypes.sort()
    elif (dataframe_as == 'shares') and (htypes is not None):
        combined_htypes = htypes
    elif (dataframe_as == 'htypes') and (shares is None):
        combined_shares = list(set(combined_shares))
        combined_shares.sort()
    elif (dataframe_as == 'htypes') and (shares is not None):
        combined_shares = shares
    combined_index = list(set(combined_index))
    htype_count = len(combined_htypes)
    share_count = len(combined_shares)
    index_count = len(combined_index)
    combined_htypes_dict = dict(zip(combined_htypes, range(htype_count)))
    combined_shares_dict = dict(zip(combined_shares, range(share_count)))
    combined_index.sort()
    # 生成并复制数据
    res_values = np.zeros(shape=(share_count, index_count, htype_count))
    res_values.fill(fill_value)
    for df_id in range(len(dfs)):
        extended_df = dfs[df_id].reindex(combined_index)
        for col_name, series in extended_df.items():  # iteritems is deprecated since 1.5 and removed since 2.0
            if dataframe_as == 'shares':
                if col_name not in combined_htypes_dict:
                    continue
                res_values[df_id, :, combined_htypes_dict[col_name]] = series.values
            else:
                if col_name not in combined_shares_dict:
                    continue
                res_values[combined_shares_dict[col_name], :, df_id] = series.values
    return HistoryPanel(res_values,
                        levels=combined_shares,
                        rows=combined_index,
                        columns=combined_htypes)


def from_df_dict(dfs: Union[list, dict], dataframe_as: str = 'shares', shares=None, htypes=None, fill_value=None):
    """ 函数stack_dataframes()的别称，等同于函数stack_dataframes()"""
    return stack_dataframes(dfs=dfs,
                            dataframe_as=dataframe_as,
                            shares=shares,
                            htypes=htypes,
                            fill_value=fill_value)


def _adjust_freq(hist_data: pd.DataFrame,
                 target_freq: str,
                 *,
                 method: str = 'last',
                 b_days_only: bool = True,
                 trade_time_only: bool = True,
                 forced_start: str = None,
                 forced_end: str = None,
                 **kwargs):
    """ 降低获取数据的频率，通过插值的方式将高频数据降频合并为低频数据，使历史数据的时间频率
    符合target_freq

    Parameters
    ----------
    hist_data: pd.DataFrame
        历史数据，是一个index为日期/时间的DataFrame
    target_freq: str
        历史数据的目标频率，包括以下选项：
         - 1/5/15/30min 1/5/15/30分钟频率周期数据(如K线)
         - H/D/W/M 分别代表小时/天/周/月 周期数据(如K线)
         如果下载的数据频率与目标freq不相同，将通过升频或降频使其与目标频率相同
    method: str
        调整数据频率分为数据降频和升频，在两种不同情况下，可用的method不同：
        数据降频就是将多个数据合并为一个，从而减少数据的数量，但保留尽可能多的信息，
        降频可用的methods有：
        - 'last'/'close': 使用合并区间的最后一个值
        - 'first'/'open': 使用合并区间的第一个值
        - 'max'/'high': 使用合并区间的最大值作为合并值
        - 'min'/'low': 使用合并区间的最小值作为合并值
        - 'mean'/'average': 使用合并区间的平均值作为合并值
        - 'sum/total': 使用合并区间的总和作为合并值

        数据升频就是在已有数据中插入新的数据，插入的新数据是缺失数据，需要填充。
        升频可用的methods有：
        - 'ffill': 使用缺失数据之前的最近可用数据填充，如果没有可用数据，填充为NaN
        - 'bfill': 使用缺失数据之后的最近可用数据填充，如果没有可用数据，填充为NaN
        - 'nan': 使用NaN值填充缺失数据
        - 'zero': 使用0值填充缺失数据
    b_days_only: bool 默认True
        是否强制转换自然日频率为工作日，即：
        'D' -> 'B'
        'W' -> 'W-FRI'
        'M' -> 'BM'
    trade_time_only: bool, 默认True
        为True时 仅生成交易时间段内的数据，交易时间段的参数通过**kwargs设定
    forced_start: str, Datetime like, 默认None
        强制开始日期，如果为None，则使用hist_data的第一天为开始日期
    forced_start: str, Datetime like, 默认None
        强制结束日期，如果为None，则使用hist_data的最后一天为结束日期
    **kwargs:
        用于生成trade_time_index的参数，包括：
        include_start:   日期时间序列是否包含开始日期/时间
        include_end:     日期时间序列是否包含结束日期/时间
        start_am:        早晨交易时段的开始时间
        end_am:          早晨交易时段的结束时间
        include_start_am:早晨交易时段是否包括开始时间
        include_end_am:  早晨交易时段是否包括结束时间
        start_pm:        下午交易时段的开始时间
        end_pm:          下午交易时段的结束时间
        include_start_pm 下午交易时段是否包含开始时间
        include_end_pm   下午交易时段是否包含结束时间

    Returns
    -------
    DataFrame:
    一个重新设定index并填充好数据的历史数据DataFrame

    Examples
    --------
    例如，合并下列数据(每一个tuple合并为一个数值，?表示合并后的数值）
        [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(?), (?), (?)]
    数据合并方法:
    - 'last'/'close': 使用合并区间的最后一个值。如：
        [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(3), (5), (7)]
    - 'first'/'open': 使用合并区间的第一个值。如：
        [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(1), (4), (6)]
    - 'max'/'high': 使用合并区间的最大值作为合并值：
        [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(3), (5), (7)]
    - 'min'/'low': 使用合并区间的最小值作为合并值：
        [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(1), (4), (6)]
    - 'avg'/'mean': 使用合并区间的平均值作为合并值：
        [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(2), (4.5), (6.5)]
    - 'sum'/'total': 使用合并区间的平均值作为合并值：
        [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(2), (4.5), (6.5)]

    例如，填充下列数据(?表示插入的数据）
        [1, 2, 3] 填充后变为: [?, 1, ?, 2, ?, 3, ?]
    缺失数据的填充方法如下:
    - 'ffill': 使用缺失数据之前的最近可用数据填充，如果没有可用数据，填充为NaN。如：
        [1, 2, 3] 填充后变为: [NaN, 1, 1, 2, 2, 3, 3]
    - 'bfill': 使用缺失数据之后的最近可用数据填充，如果没有可用数据，填充为NaN。如：
        [1, 2, 3] 填充后变为: [1, 1, 2, 2, 3, 3, NaN]
    - 'nan': 使用NaN值填充缺失数据：
        [1, 2, 3] 填充后变为: [NaN, 1, NaN, 2, NaN, 3, NaN]
    - 'zero': 使用0值填充缺失数据：
        [1, 2, 3] 填充后变为: [0, 1, 0, 2, 0, 3, 0]
    """

    if not isinstance(target_freq, str):
        err = TypeError(f'target freq should be a string, got {target_freq}({type(target_freq)}) instead.')
        raise err
    target_freq = target_freq.upper()
    # 如果hist_data为空，直接返回
    if hist_data.empty:
        return hist_data
    if b_days_only:
        if target_freq in ['W', 'W-SUN']:
            target_freq = 'W-FRI'
        elif target_freq == 'M':
            target_freq = 'BME'
    # 如果hist_data的freq与target_freq一致，也可以直接返回
    # TODO: 这里有bug：强制start/end的情形需要排除
    if hist_data.index.freqstr == target_freq:
        return hist_data
    # 如果hist_data的freq为None，可以infer freq
    if hist_data.index.inferred_freq == target_freq:
        return hist_data

    # 新版本pandas修改了部分freq alias，为了确保向后兼容，确保freq_aliases与pandas版本匹配
    target_freq = pandas_freq_alias_version_conversion(target_freq)

    # 如果target_freq为h，则实际resample频率为30min，因为需要兼顾交易日早上9:30-11:30和下午13:00-15:00两个时段
    # 如果target_freq为30min或15min等，则直接使用该freq
    resampled = hist_data.resample('30min' if target_freq == 'h' else target_freq)
    if method in ['last', 'close']:
        resampled = resampled.last()
    elif method in ['first', 'open']:
        resampled = resampled.first()
    elif method in ['max', 'high']:
        resampled = resampled.max()
    elif method in ['min', 'low']:
        resampled = resampled.min()
    elif method in ['avg', 'mean']:
        resampled = resampled.mean()
    elif method in ['sum', 'total']:
        resampled = resampled.sum()
    elif method == 'ffill':
        resampled = resampled.ffill()
    elif method == 'bfill':
        resampled = resampled.bfill()
    elif method in ['nan', 'none']:
        resampled = resampled.first()
    elif method == 'zero':
        resampled = resampled.first().fillna(0)
    else:
        # for unexpected cases
        err = ValueError(f'resample method {method} can not be recognized.')
        raise err

    # 完成resample频率切换后，根据设置去除非工作日或非交易时段的数据
    # 并填充空数据
    resampled_index = resampled.index
    if forced_start is None:
        start = resampled_index[0]
    else:
        start = pd.to_datetime(forced_start)
    if forced_end is None:
        end = resampled_index[-1]
    else:
        end = pd.to_datetime(forced_end)

    # 如果要求强制转换自然日频率为工作日频率
    # 原来的版本在resample之前就强制转换自然日到工作日，但是测试发现，pd的resample有一个bug：
    # 这个bug会导致method为last时，最后一个工作日的数据取自周日，而不是周五
    # 在实际测试中发现，如果将2020-01-01到2020-01-10之间的Hourly数据汇总到工作日时
    # 2020-01-03是周五，汇总时本来应该将2020-01-03 23:00:00的数据作为当天的数据
    # 但是实际上2020-01-05 23:00:00 的数据被错误地放置到了周五，也就是周日的数据被放到
    # 了周五，这样可能会导致错误的结果
    # 因此解决方案是，仍然按照'D'频率来resample，然后再通过reindex将非交易日的数据去除
    # 不过仅对freq为'D'的频率如此操作
    if b_days_only:
        if target_freq == 'D':
            target_freq = 'B'

    # 如果要求去掉非交易时段的数据
    from qteasy.trading_util import trade_time_index
    if trade_time_only:
        expanded_index = trade_time_index(
                start=start,
                end=end,
                freq=target_freq,
                trade_days_only=b_days_only,
                **kwargs
        )
    else:
        expanded_index = pd.date_range(start=start, end=end, freq=target_freq)
    resampled = resampled.reindex(index=expanded_index)
    # 如果在数据开始或末尾增加了空数据（因为forced start/forced end），需要根据情况填充
    if (expanded_index[-1] > resampled_index[-1]) or (expanded_index[0] < resampled_index[0]):
        if method == 'ffill':
            resampled.ffill(inplace=True)
        elif method == 'bfill':
            resampled.bfill(inplace=True)
        elif method == 'zero':
            resampled.fillna(0, inplace=True)

    return resampled


# ==================
# High level functions that creates HistoryPanel that fits the requirement of trade strategies
# ==================
def get_history_data_packages(
        data_types: list[DataType],
        data_source: DataSource,
        shares,
        start=None,
        end=None,
        rows=None,
) -> dict[str, pd.DataFrame]:
    """ 历史数据获取函数，从本地DataSource（数据库/csv/hdf/fth）获取所需的数据并返回一个
    data_package（包含不同数据类型的区间数据），返回的数据类型为dict，包含每一个data_type
    的历史数据，其columns包括所有的shares。其index为start到end之间的DatatimeIndex，同时
    根据历史数据的类型，数据将被赋予正确的TimeOffset。

    关于TimeOffset：当数据源中的数据频率低于等于日频时，其index标签都仅包含日期，当转换为包含
    时间的index时，时间部分会被置为00:00:00，如20200101会被置为2020-01-01 00:00:00，这与
    事实不符，因为2020-01-01的数据不会在当天0点就可用，因此需要手动设置一个Offset以确定该数据
    的真正可用时间，如收盘价、最高价、最低价在每天的15:00可用，开盘价在每天9:30可用。因此如果读
    取close_E_d的数据，其DatetimeIndex的时间部分应该为 "2020-01-01 15:00:00"，而
    open_E_d的数据的DatetimeIndex应该为"2020-01-01 09:30:00"

    Parameters
    ----------
    data_types: [DataType]
        需要获取的历史数据类型集合，必须是合法的DataType数据类型对象，
    data_source: DataSource
        数据源对象，用于获取数据
    shares: [str, list]
        需要获取历史数据的证券代码集合，可以是以逗号分隔的证券代码字符串或者证券代码字符列表，
        如以下两种输入方式皆合法且等效：
         - str:     '000001.SZ, 000002.SZ, 000004.SZ, 000005.SZ'
         - list:    ['000001.SZ', '000002.SZ', '000004.SZ', '000005.SZ']
    start: str
        YYYYMMDD HH:MM:SS 格式的日期/时间，获取的历史数据的开始日期/时间
    end: str
        YYYYMMDD HH:MM:SS 格式的日期/时间，获取的历史数据的结束日期/时间
    rows: int, optional
        获取的历史数据的行数
        如果rows为正整数，则获取最近的rows行历史数据，如果给出了start或end参数，则忽略rows参数

    Returns
    -------
    dict of DataFrames:
        一个dict，key为data_type.name，value为对应data_type的数据DataFrame

    Examples
    --------
    >>> from qteasy import DataType, DataSource

    """
    if isinstance(data_types, DataType):
        data_types = [data_types]
    if not isinstance(data_types, (list, tuple)):
        raise TypeError(f'data_types should be a list or tuple of DataType, got {type(data_types)} instead.')
    if not all([isinstance(dt, DataType) for dt in data_types]):
        raise TypeError(f'all elements of data_types should be DataType, got {data_types} instead.')

    if shares is None:
        shares = []
    if isinstance(shares, str):
        shares = str_to_list(shares, sep_char=',')

    symbolized_dtypes = [dt for dt in data_types if not dt.unsymbolized]
    unsymbolized_dtypes = [dt for dt in data_types if dt.unsymbolized]

    # 当我们使用unsymbolizer作为是否获取参考数据的依据时，shares就可以为空列表，但是前提是symbolized_dtypes为空
    if len(shares) == 0 and len(symbolized_dtypes) > 0:
        raise ValueError(f'shares can not be an empty list while symbolized data types are given: {symbolized_dtypes}.')

    all_dfs = {}
    # 获取针对shares的symbolized数据
    if len(symbolized_dtypes) > 0:
        all_dfs.update(get_history_data_from_source(
                datasource=data_source,
                htypes=symbolized_dtypes,
                qt_codes=list_to_str_format(shares),
                start=start,
                end=end,
                row_count=rows,
        ))
    # 获取无share的参考数据
    if len(unsymbolized_dtypes) > 0:
        # 获取无share数据
        all_dfs.update(get_reference_data_from_source(
                datasource=data_source,
                htypes=unsymbolized_dtypes,
                start=start,
                end=end,
                row_count=rows,
        ))

    # 根据数据类型设置数据日期时间的Offset值
    for dt in data_types:
        time_availability_offset = dt.get_available_time()
        if time_availability_offset is not None:
            df = all_dfs[dt.dtype_id]
            try:
                df.index = df.index + pd.Timedelta(time_availability_offset)
            except:
                # import pdb
                # pdb.set_trace()
                pass
            all_dfs[dt.dtype_id] = df

    return all_dfs


def get_history_panel(
        data_types: list[DataType],
        data_source: DataSource,
        shares: Optional[Union[str, list[str]]]=None,
        freq=None,
        start=None,
        end=None,
        rows=None,
        drop_nan=True,
        resample_method='ffill',
        b_days_only=True,
        trade_time_only=True,
        return_history_panel=True,
        **kwargs
) -> Union[HistoryPanel, dict[str, pd.DataFrame]]:
    """ 历史数据获取函数，从本地DataSource（数据库/csv/hdf/fth）获取所需的数据并组装为一个
    HistoryPanel数据对象，该HistoryPanel的数据时间频率由参数指定，查找数据时会自动匹配相应
    的数据类型，如果没有完全匹配频率的数据类型，则会找到最近的类型并通过升频或降频的方式调整为
    所需频率输出。

    只要给出数据类型、数据源和证券代码，就可以直接获取所需的历史数据，这个函数存在的意义在于：
    有些数据类型有相同的名字，但是对应不同的频率或资产类型，在读取数据时，这些不同的资产类型
    会被分别处理并存储在不同的数据层中，但我们有时候需要将它们合并起来，例如，股票000001.SZ和
    指数000001.SH都有close类型的数据，但它们是存储在不同的数据表中的不同数据类型。但我们有时候
    希望在close数据层中同时看到股票和指数的close数据，这时就需要将它们合并到同一个数据层中。

    这个函数在读取数据时，会根据数据类型的名字将不同的数据类型合并到一起，例如close_E_d的数据
    会跟close_IDX_d的数据合并，成为同一层的数据并给它一个统一的标签'close'.这样就能在同一层
    中同时看到股票和指数的close数据了。

    同时，这个函数还会根据需要调整数据的频率，将所有不同频率的数据全部都统一为需要的频率输出。
    如果某些数据没有指定频率的数据类型，则会找到频率最接近的类型并通过升频或降频的方式调整为
    需要的频率输出。

    如果选择合并数据类型，则必须确保输入测数据类型不包括多个相同名字不同频率的数据类型，因为
    这种情况会导致错误：例如，输入数据类型包括close_E_d和close_E_h，
    这时在输入处理阶段，程序会检查是否存在多个相同名字的数据类型，如果存在，则只保留频率最接近
    目标频率的那个数据类型，其他相同名字的数据类型会被忽略掉。

    TODO: 将data_types参数改为data_type_names参数，只需要传入数据类型的名称列表即可，
     由函数内部根据名称、频率和shares的资产类型，通过infer_asset_types()函数来自动匹配
     合适的数据类型对象

    Parameters
    ----------
    data_types: [DataType]
        需要获取的历史数据类型的名称，
    data_source: DataSource
        数据源对象，用于获取数据
    shares: [str, list]
        需要获取历史数据的证券代码集合，可以是以逗号分隔的证券代码字符串或者证券代码字符列表，
        如以下两种输入方式皆合法且等效：
         - str:     '000001.SZ, 000002.SZ, 000004.SZ, 000005.SZ'
         - list:    ['000001.SZ', '000002.SZ', '000004.SZ', '000005.SZ']
    freq: str, optional
        如果给出此参数，则强制转换获取的历史数据的频率，此时输出的所有数据全部为同频率：
         - 1/5/15/30min 1/5/15/30分钟频率周期数据(如K线)
         - H/D/W/M 分别代表小时/天/周/月 周期数据(如K线)
        否则输出的历史数据频率与数据类型中规定的频率相同
    start: str datetime like
        YYYYMMDD HH:MM:SS 格式的日期/时间，获取的历史数据的开始日期/时间(如果可用)
    end: str datetime like
        YYYYMMDD HH:MM:SS 格式的日期/时间，获取的历史数据的结束日期/时间(如果可用)
    rows: int
        获取的历史数据的行数，如果rows为None，则获取所有可用的历史数据
        如果rows为正整数，则获取最近的rows行历史数据，如果给出了start或end参数，则忽略rows参数
    drop_nan: bool
        是否保留全NaN的行
    resample_method: str
        如果数据需要升频或降频时，调整频率的方法
        调整数据频率分为数据降频和升频，在两种不同情况下，可用的method不同：
        数据降频就是将多个数据合并为一个，从而减少数据的数量，但保留尽可能多的信息，
        例如，合并下列数据(每一个tuple合并为一个数值，?表示合并后的数值）
            [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(?), (?), (?)]
        数据合并方法:
        - 'last'/'close': 使用合并区间的最后一个值。如：
            [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(3), (5), (7)]
        - 'first'/'open': 使用合并区间的第一个值。如：
            [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(1), (4), (6)]
        - 'max'/'high': 使用合并区间的最大值作为合并值：
            [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(3), (5), (7)]
        - 'min'/'low': 使用合并区间的最小值作为合并值：
            [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(1), (4), (6)]
        - 'avg'/'mean': 使用合并区间的平均值作为合并值：
            [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(2), (4.5), (6.5)]
        - 'sum'/'total': 使用合并区间的平均值作为合并值：
            [(1, 2, 3), (4, 5), (6, 7)] 合并后变为: [(2), (4.5), (6.5)]

        数据升频就是在已有数据中插入新的数据，插入的新数据是缺失数据，需要填充。
        例如，填充下列数据(?表示插入的数据）
            [1, 2, 3] 填充后变为: [?, 1, ?, 2, ?, 3, ?]
        缺失数据的填充方法如下:
        - 'ffill': 使用缺失数据之前的最近可用数据填充，如果没有可用数据，填充为NaN。如：
            [1, 2, 3] 填充后变为: [NaN, 1, 1, 2, 2, 3, 3]
        - 'bfill': 使用缺失数据之后的最近可用数据填充，如果没有可用数据，填充为NaN。如：
            [1, 2, 3] 填充后变为: [1, 1, 2, 2, 3, 3, NaN]
        - 'nan': 使用NaN值填充缺失数据：
            [1, 2, 3] 填充后变为: [NaN, 1, NaN, 2, NaN, 3, NaN]
        - 'zero': 使用0值填充缺失数据：
            [1, 2, 3] 填充后变为: [0, 1, 0, 2, 0, 3, 0]
    b_days_only: bool 默认True
        是否强制转换自然日频率为工作日，即：
        'D' -> 'B'
        'W' -> 'W-FRI'
        'M' -> 'BM'
    trade_time_only: bool, 默认True
        为True时 仅生成交易时间段内的数据，交易时间段的参数通过**kwargs设定
    resample_method: str
        处理数据频率更新时的方法
    return_history_panel: bool, default True
        是否返回HistoryPanel对象，如果为False，则返回一个dict，dict的key为data
    **kwargs:
        用于生成trade_time_index的参数，包括：
        include_start:   日期时间序列是否包含开始日期/时间
        include_end:     日期时间序列是否包含结束日期/时间
        start_am:        早晨交易时段的开始时间
        end_am:          早晨交易时段的结束时间
        include_start_am:早晨交易时段是否包括开始时间
        include_end_am:  早晨交易时段是否包括结束时间
        start_pm:        下午交易时段的开始时间
        end_pm:          下午交易时段的结束时间
        include_start_pm 下午交易时段是否包含开始时间
        include_end_pm   下午交易时段是否包含结束时间

    Returns
    -------
    DataFrame
    """
    if not data_types:
        return HistoryPanel()

    if data_source is None:
        raise TypeError(f'A data source should be given to acquire data from!')

    if freq is not None:
        different_freq_dtypes = [dtype for dtype in data_types if dtype.freq != freq]
        if different_freq_dtypes:
            #     warn(f'Some datatypes ({different_freq_dtypes}) do not match the frequency '
            #          f'you requested to acquire({freq}),\n'
            #          f'They will be re-sampled to match requested frequency!')
            pass

    if shares:
        # 在这里获取有share的数据，但是注意，因为这里选择将相同name但是不同资产类型的数据合并到一起
        # 但如果相同name的数据类型中有多个不同频率，则会在下面的函数中报错，此时应该检查输入的数据类型
        # 并调整其频率，确保每一个name只有一个频率的数据类型
        normal_dfs = get_history_data_from_source(
                datasource=data_source,
                htypes=data_types,
                qt_codes=list_to_str_format(shares),
                start=start,
                end=end,
                freq=freq,
                row_count=rows,
                combine_asset_types=True,
        )
        all_dfs = normal_dfs
    else:
        # 获取无share数据，且数据使用d_type.name为key分组
        reference_dfs = get_reference_data_from_source(
                datasource=data_source,
                htypes=data_types,
                start=start,
                end=end,
                freq=freq,
                row_count=rows,
                group_by_dtype_name=True,
        )
        all_dfs = reference_dfs

    # 处理所有的df，根据设定执行以下几个步骤：
    #  2，检查整行NaN值的情况，根据设定去掉或保留这些行
    #  3，如果df是一个Series，则将其转化为DataFrame
    for htyp, df in all_dfs.items():
        if isinstance(df, pd.Series):
            df = pd.DataFrame(df)
            df.columns = ['none']
        # find freq of the htyp:
        htype_freq = [d_type for d_type in data_types if d_type.name == htyp][0]
        if (not b_days_only) or (not trade_time_only) or (freq != htype_freq.freq):
            new_df = _adjust_freq(
                    df,
                    target_freq=freq,
                    method=resample_method,
                    forced_start=start,
                    forced_end=end,
                    b_days_only=b_days_only,
                    trade_time_only=trade_time_only,
                    **kwargs
            )
            df = new_df
        if rows is not None:
            assert isinstance(rows, int)
            assert rows > 0
            df = df.tail(rows)
        if drop_nan:
            df = df.dropna(how='all')
        all_dfs[htyp] = df
    if return_history_panel:
        result_hp = stack_dataframes(all_dfs, dataframe_as='htypes', htypes=all_dfs.keys(), shares=shares)
        if rows is not None:
            assert isinstance(rows, int)
            assert rows > 0
            result_hp = result_hp.tail(rows)
        return result_hp
    else:
        return all_dfs


# =================================================
# 以下是一些独立的函数, 用于检查和准备历史数据
# =================================================
def check_and_prepare_live_trade_data(op,
                                      trade_date: Union[str, pd.Timestamp],
                                      datasource: DataSource,
                                      shares: Union[str, list[str]],
                                      live_prices: Optional[pd.DataFrame] = None) -> dict[str, pd.DataFrame]:
    """ 在run_mode == 0的情况下准备相应的历史数据

    Parameters
    ----------
    op: Operator
        需要设置数据的Operator对象
    trade_date: str, pd.Timestamp
        交易日期
    datasource: DataSource
        用于下载数据的DataSource对象
    shares: str or list of str
        股票代码清单，逗号分隔字符串或字符串列表
    live_prices: pd.DataFrame, optional
        用于实盘交易的最新价格数据，如果不提供，则从datasource中下载获取

    Returns
    -------
    hist_op: HistoryPanel
        用于回测的历史数据，包含用于计算交易结果的所有历史价格种类
    hist_ref: HistoryPanel
        用于回测的历史参考数据，包含用于计算交易结果的所有历史参考数据
    """

    return check_and_prepare_backtest_data(
            op=op,
            backtest_start=trade_date,
            backtest_end=trade_date,
            shares=shares,
            datasource=datasource,
    )


def check_and_prepare_backtest_data(op,
                                    backtest_start: str,
                                    backtest_end: str,
                                    shares: Union[str, list[str]],
                                    datasource: DataSource) -> dict[str, pd.DataFrame]:
    """ 生成operator对象在回测模式下运行所需要的相关数据包，包括回测所有交易策略所需的历史数据，
    遍历Operator对象中的所有交易策略，获取所有交易策略所需要的所有历史数据。

    这个函数的要点在于解析operator对象中所有策略的数据窗口长度和数据频率，获取的数据必须可以生成足够的
    历史数据窗口以覆盖回测开始日期到回测结束日期之间的所有数据。例如：
    假设回测区间为20200101到20201231，某个策略需要20天的日线数据窗口，那么获取的历史数据起始日期
    应该至少为20191211，以保证在20200101日可以获取到20天的历史数据窗口。

    Parameters
    ----------
    op: qteasy.Operator
        交易员对象，包含投资策略信息
    backtest_start: str
        回测开始日期，格式为 'YYYYMMDD'
    backtest_end: str
        回测结束日期，格式为 'YYYYMMDD'
    shares: list of str
        回测资产池中的股票列表
    datasource: qteasy.DataSource
        数据源对象

    Returns
    -------
    hist_data_package: dict{str, pd.DataFrame}
        包含回测所需的历史数据和资金计划的字典，键为股票代码，值为对应的历史数据DataFrame
    """

    # 根据投资回测区间的开始日期及结束日期，确定需要获取的历史数据的起止日期（因为获取的数据必须覆盖交易策略的最大窗口长度）
    invest_start, invest_end = backtest_start, backtest_end

    # 获取回测所需历史数据的参数
    data_types = op.all_strategy_data_types

    if not data_types:
        return {}

    # 计算数据窗口偏移长度，这个长度需要扣除非交易日，并考虑到低频率数据的影响
    max_window_length = op.max_window_length
    time_offset_multipliers = {
        '1min':     3/240,
        '5min':     3/48,
        '15min':    2/16,
        '30min':    2/8,
        'h':        2/4,
        'd':        1.5,
        'w':        9.,
        'm':        35.,
        'q':        130.,
        'y':        360.,
    }
    max_multiplier = int(np.ceil(max(time_offset_multipliers.get(dt.freq, 1.) for dt in data_types)))
    time_window_delta = pd.Timedelta(max_window_length * max_multiplier * 2, 'D')

    # 通过get_history_data_package函数获取数据类型的原始数据
    hist_data_package = get_history_data_packages(
            data_types=data_types,
            shares=shares,
            start=regulate_date_format(pd.to_datetime(invest_start) - time_window_delta),
            end=invest_end,
            data_source=datasource,
    )

    return hist_data_package


def check_and_prepare_trade_prices(op,
                                   shares: Union[str, list[str]],
                                   price_adj: str,
                                   datasource: DataSource) -> pd.DataFrame:
    """ 基于Operator对象已经生成的group_schedule，获取指定时间区间内的交易价格数据。

    这个函数的要点在于根据operator对象中交易策略的运行时间表，获取每个运行时间点上的历史价格。如果
    operator对象的交易策略有多个运行频率，产生的运行时间表是多个频率的时间点的并集，那么获取的交易价格数据
    也必须覆盖所有这些时间点。此时应该根据运行时间表推测最高频率，获取最高频率交易价格后，再通过search_sorted
    的方式获取所有运行时间点上的交易价格。

    需要注意的是日频以及更低频率的交易价格，必须考虑到这些价格的可用时间，例如收盘价在当天15:00可用，开盘价在当天
    09:30可用。因此在获取日频或更低的交易价格时，需要将价格的时间点调整到对应的可用时间点上。

    Parameters
    ----------
    op: qteasy.Operator
        交易员对象，包含投资策略信息
    shares: list of str
        资产池中的股票列表
    price_adj: str
        价格复权类型，'none' 表示不复权，'back' 表示后复权，'forward' 表示前复权，其余值将引发错误
    datasource: qteasy.DataSource
        数据源对象
    Returns
    -------
    trade_prices: pd.DataFrame
        包含用于回测的交易价格数据
    """
    # 检查Operator对象是否已经创建了交易时间表，如果还没有创建时间表，则报错
    op_group_schedules = op.group_schedules  # 一个dict，每个group为key，一个DataFrame为value
    if not op_group_schedules:
        raise ValueError(f'Operator object has no group schedules, please run op.create_group_schedules() first!')
    if not isinstance(price_adj, str):
        raise TypeError(f'price_adj should be a string, got {type(price_adj)} instead')
    if price_adj.lower() not in ['none', 'n', 'back', 'b', 'forward', 'f', 'accu']:
        raise ValueError(f"invalid price_adj ({price_adj}), which should be anyone of ['none', 'back', 'forward']")
    if isinstance(shares, str):
        shares = str_to_list(shares, sep_char=',')

    all_run_freqs = [group.run_freq for group in op.groups.values()]
    all_run_timings = [group.run_timing for group in op.groups.values()]
    all_schedules = [sched.index for sched in op.group_schedules.values()]
    all_group_ids = [group.name for group in op.groups.values()]
    price_start = min(sched[0] for sched in all_schedules).date() - pd.Timedelta(1, 'd')  # 多取一天以防止时间点在当天的情况
    price_end = max(sched[-1] for sched in all_schedules).date() + pd.Timedelta(1, 'd')  # 多取一天以防止时间点在当天的情况

    trade_prices_per_group = {}

    for freq, timing, sched, group_id in zip(all_run_freqs, all_run_timings, all_schedules, all_group_ids):
        # 生成需要获取的数据的数据类型，以便使用get_history_panel函数获取数据
        data_types = []
        asset_types = 'E, IDX'

        fund_shares = [symbol for symbol in shares if symbol[-2:] in ['OF']]
        dtype_freq = freq

        if (timing == 'open') and freq[0].lower() in ['d', 'w', 'm', 'q', 'y']:  # 日频及更低频率的开盘价使用前一交易日的
            price_data_type_name = 'open'
            dtype_freq = 'd'
        elif (timing == 'close') and freq[0].lower() in ['d', 'w', 'm', 'q', 'y']:  # 日频及更低频率的收盘价使用当日收盘价
            price_data_type_name = 'close'
            dtype_freq = 'd'
        elif (timing not in ['open', 'close']) and freq[0].lower() in ['d', 'w', 'm', 'q']:  # 日频及更低频率的其他时间点使用分钟线价格
            price_data_type_name = 'close'
            dtype_freq = '1min'
        else:  # 其他情况使用当时的价格
            price_data_type_name = 'close'

        if fund_shares:  # 如果有场外基金，使用单位净值或复权作为交易价格
            if price_adj == 'none':
                price_data_type_name += ', unit_nav'
            elif price_adj == 'back':
                price_data_type_name += ', accum_nav'
            else:  # price_adj == 'forward'
                price_data_type_name += ', adj_nav'
            asset_types += ', FD'

        # 获取统一时间频率价格
        data_types.extend(
                infer_data_types(
                        price_data_type_name,
                        freqs=dtype_freq,
                        asset_types=asset_types,
                        adj=price_adj,
                        allow_ignore_freq=True,
                        allow_ignore_adj=True,
                )
        )

        # 生成回测交易价格的数据参数, 交易价格的复权类型根据price_adj参数确定
        trade_prices = get_history_panel(
                data_types=data_types,
                data_source=datasource,
                shares=shares,
                freq=dtype_freq,
                start=price_start,
                end=price_end,
                resample_method='ffill',
                return_history_panel=False,
        )

        # 此时有两种情况，一种是获取的价格数据包含多个数据类型（如开盘价和累计净值），另一种是只包含一个数据类型
        # 需要分别处理，如果包含多个数据类型，则需从多个DataFrame中选择合适的价格进行合并
        if len(trade_prices) == 1:
            price_data_type_name = list(trade_prices.keys())[0]
            trade_prices = trade_prices[price_data_type_name]
        elif len(trade_prices) > 1:  # 此时检查所有的数据列，删除全部为NaN的列，然后将剩下的列进行合并，并补齐缺少的列（这种情况说明有数据未读到，后面应报错）
            for dtype_name, df in trade_prices.items():
                all_nan_cols = df.columns[df.isna().all()].tolist()
                if all_nan_cols:
                    trade_prices[dtype_name] = df.drop(columns=all_nan_cols)
            # 将trade_prices中剩下的DataFrame进行合并
            trade_prices = pd.concat(list(trade_prices.values()), axis=1)
            trade_prices = trade_prices.reindex(columns=shares)
        else:
            raise ValueError(f'Unexpected number of data types ({len(trade_prices)}) in trade prices!')

        # 调整日频及更低频率数据的时间点到对应的可用时间点上
        if dtype_freq.lower() in ['d', 'w', 'm', 'q', 'y']:
            # 从schedules中获取对应的时间可用偏移量
            time_offset = sched[0] - pd.to_datetime(sched[0].date())
            trade_prices.index = trade_prices.index + pd.Timedelta(time_offset)

        trade_prices_per_group[group_id] = trade_prices

    # 当多个交易组存在时，合并各个交易组的交易价格数据，取并集
    all_trade_price_indices = pd.Index([])
    for sched in op_group_schedules.values():
        all_trade_price_indices = all_trade_price_indices.union(sched.index)
    all_trade_price_indices = pd.to_datetime(all_trade_price_indices.sort_values())

    if len(trade_prices_per_group) > 1:

        combined_trade_prices = pd.concat(trade_prices_per_group.values())
        combined_trade_prices = combined_trade_prices.groupby(level=0).first()

    else:
        combined_trade_prices = list(trade_prices_per_group.values())[0]

    # 对combined_trade_prices进行前向填充并确保包含所有交易时间点
    combined_trade_prices = combined_trade_prices.reindex(all_trade_price_indices)
    combined_trade_prices.ffill(inplace=True)
    # 强制列对齐到完整资产池，避免 backtest_price_adj 等导致列数不足
    combined_trade_prices = combined_trade_prices.reindex(columns=shares)

    return combined_trade_prices


def check_and_prepare_evaluate_price_data(op,
                                          shares: Union[str, list[str]],
                                          datasource: DataSource,
                                          backtest_start,
                                          backtest_end,
                                          backtest_price_adj: str) -> pd.DataFrame:
    """ 基于Operator对象已经生成的group_schedule，获取指定时间区间内的日频收盘价数据，用于回测结果评价。

    本函数通过operator信息获取用于回测结果评价的参考价格数据。与用于回测交易过程的trade_prices不同，
    这里获取的价格数据以日频交易日为时间索引，时间点统一为当日收盘时间15:00:00，主要用于回测结果的
    日度估值和业绩评价，不直接参与回测交易过程。

    Parameters
    ----------
    op: qteasy.Operator
        交易员对象，包含投资策略信息
    shares: list of str
        资产池中的股票列表
    datasource: qteasy.DataSource
        数据源对象
    backtest_start:
        回测开始日期
    backtest_end:
        回测结束日期
    backtest_price_adj: str
        回测价格复权方式，必须与回测过程中使用的价格复权方式保持一致

    Returns
    -------
    evaluate_price_data: pd.DataFrame
        包含用于回测结果评价的日频收盘价数据，索引为交易日15:00:00，列为资产代码
    """

    from qteasy.trading_util import trade_time_index

    if isinstance(shares, str):
        shares = [shares]

    start_date = pd.to_datetime(backtest_start).date()
    end_date = pd.to_datetime(backtest_end).date()

    daily_index = trade_time_index(start=start_date,
                                   end=end_date,
                                   freq='d',
                                   time_offset='15:00')

    # 根据回测配置中的backtest_price_adj确定评价用价格的复权方式，确保与回测价格保持一致
    price_adj = str(backtest_price_adj).lower()
    data_types = infer_data_types(
            'close',
            freqs='d',
            asset_types='E, IDX, FD',
            adj=price_adj,
            allow_ignore_freq=True,
            allow_ignore_adj=True,
    )

    price_panel = get_history_panel(
            data_types=data_types,
            data_source=datasource,
            shares=shares,
            freq='d',
            start=start_date - pd.Timedelta(1, 'd'),
            end=end_date + pd.Timedelta(1, 'd'),
            resample_method='ffill',
            return_history_panel=False,
    )

    if not price_panel:
        raise ValueError(f'No evaluate price data found for shares {shares}!')

    if len(price_panel) == 1:
        dtype_name = list(price_panel.keys())[0]
        evaluate_prices = price_panel[dtype_name]
    else:
        for dtype_name, df in price_panel.items():
            all_nan_cols = df.columns[df.isna().all()].tolist()
            if all_nan_cols:
                price_panel[dtype_name] = df.drop(columns=all_nan_cols)
        evaluate_prices = pd.concat(list(price_panel.values()), axis=1)
        evaluate_prices = evaluate_prices.reindex(columns=shares)

    # 将日频数据时间点调整到收盘时间15:00:00，然后与交易日索引对齐
    evaluate_prices.index = pd.to_datetime(evaluate_prices.index) + pd.Timedelta('15:00:00')
    evaluate_prices = evaluate_prices.reindex(daily_index)
    evaluate_prices.ffill(inplace=True)

    return evaluate_prices


def check_and_prepare_benchmark_data(op,
                                     benchmark_symbol: str,
                                     datasource: DataSource,
                                     backtest_start,
                                     backtest_end) -> pd.DataFrame:
    """ 获取指定时间区间内的回测业绩评价基准数据

    本函数通过operator信息获取业绩评价基准数据。业绩评价基准可以是股票、指数或者基金，但是
    只能包含单一资产的价格数据。业绩评价数据格式与参考数据类型一致，可以是一个DataFrame，索引为时间戳，
    列为资产代码。也可以是一个Series，索引为时间戳，值为资产价格，如果资产类型为E或FD(场内基金），则
    价格为后复权价格。如果资产类型为IDX，则价格为指数点位，如果资产类型为场外基金OF，则价格为基金的复权净值。

    benchmark数据会被作为回测结果的业绩评价基准，在计算中需要与每日的回测结果做比较，因此
    benchmark数据的时间索引统一为回测区间内所有交易日的收盘时间点15:00:00。

    Parameters
    ----------
    op: qteasy.Operator
        交易员对象，包含投资策略信息
    benchmark_symbol: str
        业绩评价基准资产代码
    datasource: qteasy.DataSource
        数据源对象
    backtest_start:
        回测开始日期
    backtest_end:
        回测结束日期

    Returns
    -------
    benchmark_data: pd.DataFrame
        包含用于回测结果评价的基准数据
    """

    from qteasy.trading_util import trade_time_index

    if op.group_timing_table is None or op.group_timing_table.empty:
        raise ValueError(f'Operator object has no group schedules, please run op.create_group_schedules() first!')

    start_date = pd.to_datetime(backtest_start).date()
    end_date = pd.to_datetime(backtest_end).date()

    daily_index = trade_time_index(start=start_date,
                                   end=end_date,
                                   freq='d',
                                   time_offset='15:00')
    # 解析benchmark_symbol，根据资产类型确定数据类型名称
    if benchmark_symbol[-2:] in ['OF']:  # 场外基金
        benchmark_data_type_name = 'adj_nav'
        adj = 'none'
        asset_types = 'FD'
    elif benchmark_symbol[-2:] in ['SZ', 'SH', 'BJ']:  # 覆盖股票、场内基金或指数的情况
        benchmark_data_type_name = 'close'
        adj = 'back'
        asset_types = 'E, IDX, FD'
    else:
        raise ValueError(f'Unsupported benchmark symbol: {benchmark_symbol}, please use stock, index or fund code!')

    data_types = infer_data_types(benchmark_data_type_name,
                                  freqs='d',
                                  asset_types=asset_types,
                                  adj=adj,
                                  allow_ignore_freq=True,
                                  allow_ignore_adj=True)

    benchmark_data = get_history_panel(
            data_types=data_types,
            shares=[benchmark_symbol],
            freq='d',
            start=start_date - pd.Timedelta(1, 'd'),
            end=end_date + pd.Timedelta(1, 'd'),
            data_source=datasource,
            resample_method='ffill',
            return_history_panel=False,
    )

    # benchmark_data中可能存在空DataFrame，逐个检查后将空DataFrame删除，提取非空的DataFrame
    dtype_name_to_drop = [dtype_name for dtype_name, df in benchmark_data.items() if df.empty]
    for dtype_name in dtype_name_to_drop:
        benchmark_data.pop(dtype_name)

    if not benchmark_data:
        raise ValueError(f'No benchmark data found for symbol {benchmark_symbol}!')

    benchmark_data = benchmark_data[list(benchmark_data.keys())[0]]  # 提取DataFrame

    # 将日频数据时间点调整到收盘时间15:00:00，然后与交易日索引对齐
    benchmark_data.index = pd.to_datetime(benchmark_data.index) + pd.Timedelta('15:00:00')
    benchmark_data = benchmark_data.reindex(daily_index)
    benchmark_data.ffill(inplace=True)

    return benchmark_data
