# coding=utf-8
# ======================================
# File:     test_operator_and_strategy.py
# Author:   Jackie PENG
# Contact:  jackie.pengzhao@gmail.com
# Created:  2020-02-12
# Desc:
#   Unittest for all Operator and
#   Strategy class attributes and
#   methods.
# ======================================
import unittest

import qteasy as qt
import pandas as pd
import numpy as np
import time

from qteasy.backtest import Backtester
from qteasy.parameter import Parameter
from qteasy.group import Group
from qteasy.trading_util import trade_time_index as tti
from qteasy.finance import CashPlan

from qteasy.built_in import (
    DMA,
    MACD,
    CDL,
)
from qteasy.strategy import (
    BaseStrategy,
    RuleIterator,
    GeneralStg,
    FactorSorter,
)
from qteasy.datatypes import (
    DataType,
    StgData,
)

# test parameters and datatypes:
param1 = Parameter(
        name='param1',
        par_type='int',
        par_range=(1, 4),
        value=4,
)

param2 = Parameter(
        name='param2',
        par_type='float',
        par_range=(0.0, 1.),
        value=0.5,
)

param3 = Parameter(
        name='param3',
        par_type='enum',
        par_range=('option1', 'option2', 'option3'),
        value='option1',
)

param4 = Parameter(
        name='param4',
        par_type='array[3,]',
        par_range=(1.0, 5.0),
        value=np.array([1.0, 2.0, 3.0]),
)

dtype_1 = DataType(
        name='close',
        freq='d',
        asset_type='E'
)

dtype_2 = DataType(
        name='close',
        freq='h',
        asset_type='E'
)

dtype_3 = DataType(
        name='close',
        freq='5min',
        asset_type='E'
)

dtype_4 = DataType(
        name='close',
        freq='15min',
        asset_type='E',
)

dtype_5 = DataType(
        name='close',
        freq='w',
        asset_type='E',
)

dtype_ref = DataType(
        name='close',
        freq='d',
        asset_type='IDX',
)

dtype_dep = DataType(
        name='close',
        freq='d',
        asset_type='E',
)

# 测试数据的值不是来自于DataSource，而是直接设定，方便运算
dtype_1_data = np.array(
        [[0.994, 0.412, 0.876],
         [1.117, 1.257, 1.447],
         [2.315, 2.080, 2.799],
         [3.704, 3.870, 3.620],
         [6.091, 6.127, 6.218],
         [7.177, 7.337, 7.294],
         [8.240, 8.254, 8.626],
         [9.552, 9.446, 9.739],
         [10.754, 10.192, 10.650],
         [13.877, 13.685, 13.885],
         [14.556, 14.774, 14.250],
         [15.167, 15.026, 15.999],
         [16.580, 16.236, 16.307],
         [17.599, 17.797, 17.155],
         [27.921, 27.938, 27.481],
         [28.281, 28.604, 28.348],
         [29.062, 29.559, 29.880],
         [30.390, 30.247, 30.393],
         [31.902, 31.344, 31.878],
         [34.127, 34.099, 34.396],
         [35.332, 35.854, 35.131],
         [36.482, 36.229, 36.578],
         [37.435, 37.952, 37.289],
         [38.605, 38.377, 38.311],
         [41.676, 41.230, 41.839]],
)
dtype_2_data = np.array(
        [[0.296, 0.969, 0.422],
         [0.153, 0.306, 0.254],
         [0.793, 0.406, 0.798],
         [0.257, 0.461, 0.749],
         [1.201, 1.871, 1.381],
         [1.117, 1.665, 1.956],
         [1.040, 1.577, 1.919],
         [1.431, 1.490, 1.036],
         [2.634, 2.331, 2.445],
         [2.981, 2.116, 2.068],
         [2.646, 2.634, 2.910],
         [2.901, 2.668, 2.527],
         [3.217, 3.329, 3.680],
         [3.027, 3.806, 3.796],
         [3.047, 3.814, 3.564],
         [3.091, 3.168, 3.139],
         [6.273, 6.362, 6.544],
         [6.221, 6.621, 6.439],
         [6.990, 6.570, 6.894],
         [6.771, 6.277, 6.217],
         [7.140, 7.595, 7.823],
         [7.508, 7.924, 7.539],
         [7.065, 7.398, 7.163],
         [7.630, 7.433, 7.144],
         [8.988, 8.035, 8.454],
         [8.468, 8.776, 8.954],
         [8.057, 8.413, 8.022],
         [8.812, 8.767, 8.421],
         [9.974, 9.411, 9.576],
         [9.738, 9.484, 9.772],
         [9.390, 9.578, 9.635],
         [9.145, 9.801, 9.636],
         [10.093, 10.751, 10.501],
         [10.553, 10.952, 10.705],
         [10.803, 10.073, 10.546],
         [10.787, 10.033, 10.851],
         [13.340, 13.651, 13.768],
         [13.672, 13.798, 13.485],
         [13.715, 13.170, 13.580],
         [13.102, 13.216, 13.235],
         [14.304, 14.915, 14.385],
         [14.386, 14.645, 14.360],
         [14.569, 14.930, 14.235],
         [14.492, 14.152, 14.376],
         [15.367, 15.022, 15.234],
         [15.320, 15.186, 15.298],
         [15.252, 15.693, 15.582],
         [15.146, 15.020, 15.932],
         [16.687, 16.209, 16.238],
         [16.571, 16.912, 16.015],
         [16.118, 16.238, 16.120],
         [16.270, 16.909, 16.950],
         [17.636, 17.955, 17.647],
         [17.659, 17.512, 17.246],
         [17.531, 17.214, 17.680],
         [17.356, 17.611, 17.768],
         [27.391, 27.691, 27.099],
         [27.216, 27.734, 27.063],
         [27.534, 27.533, 27.873],
         [27.745, 27.081, 27.593]]
)
dtype_3_data = np.array(
        [[0.375, 0.370, 0.692],
         [0.983, 0.857, 0.961],
         [0.537, 0.462, 0.190],
         [0.510, 0.770, 0.116],
         [0.958, 0.129, 0.363],
         [0.778, 0.350, 0.849],
         [0.330, 0.420, 0.116],
         [0.246, 0.702, 0.462],
         [0.229, 0.207, 0.969],
         [0.941, 0.340, 0.615],
         [0.375, 0.014, 0.486],
         [0.110, 0.095, 0.324],
         [0.258, 0.809, 0.266],
         [0.425, 0.767, 0.464],
         [0.679, 0.314, 0.327],
         [0.390, 0.655, 0.378],
         [0.702, 0.377, 0.891],
         [0.855, 0.844, 0.990],
         [0.468, 0.934, 0.871],
         [0.051, 0.750, 0.554],
         [0.032, 0.958, 0.530],
         [0.807, 0.790, 0.116],
         [0.004, 0.479, 0.331],
         [0.395, 0.427, 0.779],
         [0.570, 0.193, 0.505],
         [0.900, 0.955, 0.863],
         [0.534, 0.036, 0.513],
         [0.516, 0.056, 0.359],
         [0.949, 0.903, 0.867],
         [0.027, 0.270, 0.913],
         [0.298, 0.337, 0.660],
         [0.677, 0.019, 0.780],
         [0.275, 0.345, 0.396],
         [0.061, 0.017, 0.924],
         [0.129, 0.398, 0.690],
         [0.289, 0.957, 0.184],
         [0.105, 0.755, 0.637],
         [0.388, 0.963, 0.431],
         [0.381, 0.378, 0.123],
         [0.520, 0.319, 0.818],
         [0.046, 0.050, 0.433],
         [0.646, 0.411, 0.850],
         [0.513, 0.281, 0.207],
         [0.344, 0.295, 0.425],
         [0.921, 0.263, 0.786],
         [0.077, 0.217, 0.871],
         [0.850, 0.649, 0.818],
         [0.885, 0.088, 0.525],
         [1.433, 1.209, 1.523],
         [1.013, 1.897, 1.519],
         [1.558, 1.484, 1.557],
         [1.005, 1.926, 1.709],
         [1.894, 1.196, 1.827],
         [1.354, 1.431, 1.649],
         [1.559, 1.759, 1.370],
         [1.077, 1.811, 1.832],
         [1.712, 1.541, 1.213],
         [1.506, 1.822, 1.654],
         [1.198, 1.949, 1.709],
         [1.222, 1.696, 1.856],
         [1.826, 1.224, 1.837],
         [1.087, 1.511, 1.413],
         [1.501, 1.391, 1.909],
         [1.384, 1.921, 1.849],
         [1.943, 1.121, 1.308],
         [1.983, 1.953, 1.041],
         [1.995, 1.570, 1.678],
         [1.781, 1.178, 1.794],
         [1.282, 1.602, 1.640],
         [1.718, 1.058, 1.298],
         [1.556, 1.472, 1.865],
         [1.686, 1.193, 1.469],
         [1.935, 1.317, 1.392],
         [1.067, 1.622, 1.936],
         [1.874, 1.187, 1.298],
         [1.294, 1.657, 1.086],
         [1.481, 1.221, 1.532],
         [1.322, 1.798, 1.976],
         [1.924, 1.553, 1.798],
         [1.564, 1.808, 1.729],
         [1.642, 1.099, 1.421],
         [1.895, 1.096, 1.931],
         [1.729, 1.487, 1.497],
         [1.325, 1.450, 1.282],
         [1.915, 1.491, 1.853],
         [1.142, 1.633, 1.685],
         [1.259, 1.669, 1.789],
         [1.002, 1.811, 1.781],
         [1.063, 1.718, 1.183],
         [1.384, 1.473, 1.794],
         [1.984, 1.189, 1.146],
         [1.197, 1.347, 1.499],
         [1.700, 1.894, 1.294],
         [1.146, 1.029, 1.737],
         [1.525, 1.055, 1.771],
         [1.165, 1.931, 1.213],
         [2.131, 2.803, 2.135],
         [2.632, 2.242, 2.884],
         [2.887, 2.505, 2.888],
         [2.514, 2.761, 2.412],
         [2.397, 2.106, 2.236],
         [2.038, 2.077, 2.195],
         [2.357, 2.516, 2.334],
         [2.833, 2.181, 2.605],
         [2.598, 2.510, 2.976],
         [2.296, 2.303, 2.914],
         [2.724, 2.226, 2.876],
         [2.146, 2.895, 2.770],
         [2.577, 2.967, 2.459],
         [2.258, 2.198, 2.924],
         [2.528, 2.151, 2.776],
         [2.901, 2.428, 2.869],
         [2.739, 2.544, 2.208],
         [2.754, 2.148, 2.708],
         [2.937, 2.628, 2.310],
         [2.464, 2.173, 2.258],
         [2.095, 2.613, 2.428],
         [2.079, 2.980, 2.146],
         [2.810, 2.864, 2.956],
         [2.629, 2.845, 2.401],
         [2.372, 2.331, 2.970],
         [2.571, 2.230, 2.877],
         [2.600, 2.421, 2.216],
         [2.749, 2.904, 2.195],
         [2.185, 2.628, 2.208],
         [2.935, 2.545, 2.074],
         [2.228, 2.898, 2.552],
         [2.306, 2.569, 2.771],
         [2.837, 2.099, 2.138],
         [2.434, 2.215, 2.945],
         [2.455, 2.709, 2.964],
         [2.573, 2.327, 2.824],
         [2.510, 2.889, 2.025],
         [2.158, 2.434, 2.110],
         [2.977, 2.127, 2.505],
         [2.959, 2.013, 2.220],
         [2.315, 2.814, 2.836],
         [2.791, 2.891, 2.459],
         [2.796, 2.187, 2.012],
         [2.347, 2.809, 2.569],
         [2.204, 2.624, 2.718],
         [2.644, 2.461, 2.504],
         [2.604, 2.019, 2.217],
         [2.050, 2.582, 2.758],
         [3.596, 3.793, 3.358],
         [3.681, 3.141, 3.088],
         [3.198, 3.032, 3.215],
         [3.768, 3.493, 3.155],
         [3.481, 3.702, 3.047],
         [3.224, 3.762, 3.572],
         [3.435, 3.432, 3.632],
         [3.168, 3.707, 3.589],
         [3.572, 3.775, 3.622],
         [3.692, 3.738, 3.133],
         [3.711, 3.733, 3.029],
         [3.621, 3.909, 3.529],
         [3.908, 3.253, 3.836],
         [3.071, 3.818, 3.627],
         [3.163, 3.677, 3.384],
         [3.686, 3.725, 3.747],
         [3.217, 3.133, 3.366],
         [3.659, 3.798, 3.597],
         [3.400, 3.885, 3.181],
         [3.840, 3.265, 3.905],
         [3.785, 3.116, 3.716],
         [3.429, 3.995, 3.700],
         [3.102, 3.577, 3.894],
         [3.859, 3.471, 3.924],
         [3.073, 3.426, 3.772],
         [3.484, 3.362, 3.732],
         [3.905, 3.197, 3.809],
         [3.782, 3.207, 3.759],
         [3.558, 3.547, 3.569],
         [3.956, 3.236, 3.780],
         [3.909, 3.511, 3.339],
         [3.787, 3.820, 3.105],
         [3.995, 3.039, 3.309],
         [3.580, 3.367, 3.577],
         [3.337, 3.210, 3.144],
         [3.802, 3.840, 3.544],
         [3.807, 3.947, 3.634],
         [3.478, 3.082, 3.435],
         [3.428, 3.279, 3.150],
         [3.183, 3.067, 3.299],
         [3.493, 3.589, 3.035],
         [3.815, 3.557, 3.462],
         [3.019, 3.028, 3.863],
         [3.223, 3.597, 3.738],
         [3.565, 3.076, 3.706],
         [3.309, 3.221, 3.165],
         [3.171, 3.399, 3.176],
         [3.235, 3.470, 3.381]]
)
dtype_4_data = np.array(
        [[3.496, 3.578, 3.001],
         [3.026, 3.351, 3.198],
         [3.678, 3.386, 3.985],
         [3.357, 3.467, 3.014],
         [3.319, 3.782, 3.705],
         [3.356, 3.091, 3.209],
         [3.122, 3.918, 3.729],
         [3.094, 3.073, 3.343],
         [3.611, 3.083, 3.345],
         [3.978, 3.881, 3.738],
         [3.206, 3.035, 3.697],
         [3.567, 3.524, 3.884],
         [3.966, 3.796, 3.849],
         [3.225, 3.695, 3.128],
         [3.540, 3.954, 3.556],
         [3.501, 3.598, 3.956],
         [6.311, 6.261, 6.459],
         [6.707, 6.215, 6.338],
         [6.019, 6.607, 6.370],
         [6.966, 6.234, 6.653],
         [6.786, 6.136, 6.881],
         [6.900, 6.981, 6.760],
         [6.670, 6.181, 6.087],
         [6.393, 6.691, 6.690],
         [6.105, 6.444, 6.785],
         [6.302, 6.523, 6.397],
         [6.087, 6.077, 6.015],
         [6.810, 6.072, 6.885],
         [6.220, 6.209, 6.478],
         [6.784, 6.135, 6.384],
         [6.811, 6.581, 6.935],
         [6.829, 6.697, 6.587],
         [7.008, 7.664, 7.347],
         [7.465, 7.989, 7.290],
         [7.799, 7.123, 7.340],
         [7.520, 7.206, 7.754],
         [7.249, 7.279, 7.512],
         [7.024, 7.334, 7.183],
         [7.356, 7.361, 7.163],
         [7.571, 7.723, 7.762],
         [7.852, 7.495, 7.681],
         [7.789, 7.230, 7.191],
         [7.882, 7.223, 7.282],
         [7.704, 7.756, 7.364],
         [7.296, 7.051, 7.166],
         [7.185, 7.655, 7.670],
         [7.517, 7.064, 7.100],
         [7.080, 7.098, 7.718],
         [8.572, 8.991, 8.070],
         [8.049, 8.279, 8.446],
         [8.992, 8.329, 8.424],
         [8.372, 8.480, 8.991],
         [8.234, 8.016, 8.956],
         [8.260, 8.751, 8.770],
         [8.377, 8.053, 8.150],
         [8.526, 8.540, 8.699],
         [8.800, 8.442, 8.388],
         [8.845, 8.364, 8.188],
         [8.011, 8.449, 8.809],
         [8.192, 8.383, 8.717],
         [8.455, 8.389, 8.182],
         [8.469, 8.272, 8.698],
         [8.887, 8.165, 8.732],
         [8.292, 8.677, 8.789],
         [9.506, 9.555, 9.518],
         [9.549, 9.890, 9.250],
         [9.107, 9.455, 9.229],
         [9.926, 9.772, 9.015],
         [9.617, 9.492, 9.495],
         [9.805, 9.823, 9.110],
         [9.204, 9.387, 9.865],
         [9.948, 9.027, 9.818],
         [9.015, 9.650, 9.124],
         [9.137, 9.699, 9.195],
         [9.662, 9.026, 9.602],
         [9.824, 9.391, 9.907],
         [9.004, 9.074, 9.118],
         [9.658, 9.596, 9.721],
         [9.160, 9.220, 9.649],
         [9.301, 9.176, 9.548],
         [10.948, 10.524, 10.069],
         [10.786, 10.199, 10.421],
         [10.806, 10.321, 10.119],
         [10.095, 10.178, 10.130],
         [10.637, 10.815, 10.054],
         [10.154, 10.894, 10.784],
         [10.724, 10.916, 10.176],
         [10.652, 10.150, 10.355],
         [10.209, 10.125, 10.323],
         [10.330, 10.619, 10.460],
         [10.680, 10.911, 10.869],
         [10.297, 10.313, 10.823],
         [10.002, 10.954, 10.198],
         [10.643, 10.256, 10.040],
         [10.477, 10.218, 10.066],
         [10.889, 10.438, 10.762],
         [13.174, 13.324, 13.294],
         [13.989, 13.918, 13.573],
         [13.944, 13.691, 13.977],
         [13.292, 13.964, 13.259],
         [13.709, 13.708, 13.709],
         [13.490, 13.309, 13.316],
         [13.103, 13.142, 13.989],
         [13.690, 13.319, 13.662],
         [13.443, 13.028, 13.810],
         [13.880, 13.006, 13.643],
         [13.032, 13.978, 13.125],
         [13.203, 13.874, 13.805],
         [13.727, 13.488, 13.435],
         [13.808, 13.819, 13.747],
         [13.582, 13.400, 13.771],
         [13.248, 13.325, 13.055],
         [14.125, 14.399, 14.519],
         [14.610, 14.894, 14.842],
         [14.054, 14.272, 14.731],
         [14.285, 14.986, 14.139],
         [14.824, 14.080, 14.709],
         [14.027, 14.595, 14.309],
         [14.301, 14.818, 14.289],
         [14.942, 14.990, 14.374],
         [14.101, 14.890, 14.207],
         [14.253, 14.705, 14.268],
         [14.217, 14.399, 14.725],
         [14.035, 14.070, 14.655],
         [14.047, 14.818, 14.061],
         [14.625, 14.998, 14.324],
         [14.120, 14.360, 14.089],
         [14.927, 14.127, 14.191],
         [15.637, 15.753, 15.807],
         [15.031, 15.856, 15.515],
         [15.091, 15.026, 15.302],
         [15.821, 15.327, 15.066],
         [15.700, 15.866, 15.226],
         [15.450, 15.249, 15.897],
         [15.456, 15.106, 15.598],
         [15.893, 15.122, 15.398],
         [15.131, 15.858, 15.872],
         [15.060, 15.793, 15.028],
         [15.744, 15.532, 15.339],
         [15.119, 15.862, 15.188],
         [15.844, 15.838, 15.060],
         [15.858, 15.167, 15.301],
         [15.222, 15.246, 15.499],
         [15.358, 15.863, 15.092],
         [16.683, 16.807, 16.261],
         [16.419, 16.759, 16.250],
         [16.398, 16.532, 16.773],
         [16.192, 16.250, 16.657],
         [16.243, 16.081, 16.615],
         [16.198, 16.900, 16.127],
         [16.539, 16.463, 16.219],
         [16.714, 16.199, 16.169],
         [16.109, 16.663, 16.129],
         [16.407, 16.546, 16.859],
         [16.011, 16.953, 16.526],
         [16.590, 16.502, 16.508],
         [16.819, 16.918, 16.637],
         [16.338, 16.277, 16.038],
         [16.579, 16.166, 16.275],
         [16.270, 16.029, 16.943],
         [17.477, 17.978, 17.552],
         [17.929, 17.826, 17.661],
         [17.314, 17.501, 17.455],
         [17.095, 17.426, 17.468],
         [17.089, 17.943, 17.504],
         [17.967, 17.234, 17.417],
         [17.412, 17.612, 17.110],
         [17.433, 17.657, 17.645],
         [17.476, 17.673, 17.100],
         [17.765, 17.185, 17.993],
         [17.613, 17.776, 17.817],
         [17.557, 17.492, 17.497],
         [17.750, 17.727, 17.281],
         [17.323, 17.904, 17.214],
         [17.117, 17.373, 17.823],
         [17.164, 17.599, 17.148]]
)
dtype_5_data = np.array(
        [[0.134, 0.207, 0.095],
         [1.015, 0.591, 0.615],
         [1.255, 0.850, 0.992]]
)

dtype_ref_data = np.array(
        [[5267.72],
         [5368.5],
         [5417.67],
         [5513.66],
         [5495.43],
         [5441.16],
         [5596.35],
         [5577.97],
         [5470.46],
         [5458.08],
         [5518.52],
         [5437.52],
         [5476.43],
         [5564.97],
         [5569.78],
         [5625.92],
         [5512.97],
         [5528.],
         [5377.14],
         [5351.96],
         [5417.65],
         [5501.09],
         [5485.2],
         [5473.95],
         [5483.41]]
)

trade_price_h_data = np.array(
        [[37.58, 32.50, 24.14],
         [38.65, 33.31, 23.80],
         [38.50, 34.20, 24.52],
         [38.38, 34.10, 23.95],
         [38.80, 34.09, 26.35],
         [38.25, 34.12, 25.96],
         [38.92, 34.43, 25.14],
         [39.05, 34.12, 24.89],
         [39.66, 34.45, 25.78],
         [40.47, 35.48, 26.37],
         [40.68, 35.45, 25.99],
         [40.38, 35.21, 25.28],
         [40.65, 35.10, 24.91],
         [41.05, 35.05, 24.92],
         [42.03, 35.05, 25.30],
         [41.21, 34.63, 25.84],
         [40.54, 34.95, 26.08],
         [40.03, 34.88, 27.32],
         [39.30, 34.70, 27.18],
         [38.35, 33.84, 27.16],
         [38.41, 34.42, 27.16],
         [38.49, 34.29, 26.85],
         [38.49, 34.35, 28.50],
         [38.16, 34.27, 29.48],
         [37.72, 34.71, 28.93],
         [38.00, 34.87, 28.71],
         [37.17, 34.54, 29.28],
         [37.27, 34.15, 29.64],
         [36.71, 33.97, 29.23],
         [38.49, 35.15, 30.44],
         [38.69, 36.14, 29.67],
         [38.16, 36.08, 30.13],
         [38.27, 37.03, 29.54],
         [37.46, 36.56, 29.42],
         [37.16, 35.21, 29.02],
         [37.31, 35.52, 28.85],
         [38.25, 36.42, 29.18],
         [38.25, 36.61, 29.51],
         [38.25, 36.97, 28.69],
         [37.00, 36.91, 28.80]]
)

close_d_df = pd.DataFrame(
        dtype_1_data, columns=['A', 'B', 'C'],
        index=tti(start='2023-01-01', end='2023-02-14', freq='D', time_offset="15:00")  # len = 25
)
close_h_df = pd.DataFrame(
        dtype_2_data, columns=['A', 'B', 'C'], index=tti(start='2023-01-01', end='2023-01-31', freq='h',
                                                         include_start_pm=False, include_start_am=False)  # len = 60
)
close_5min_df = pd.DataFrame(
        dtype_3_data, columns=['A', 'B', 'C'], index=tti(start='2023-01-01', end='2023-01-08', freq='5min',
                                                         include_start_pm=False, include_start_am=False)  # len = 192
)
close_15min_df = pd.DataFrame(
        dtype_4_data, columns=['A', 'B', 'C'], index=tti(start='2023-01-06', end='2023-01-30', freq='15min',
                                                         include_start_pm=False, include_start_am=False)  # len = 176
)
close_w_df = pd.DataFrame(
        dtype_5_data, columns=['A', 'B', 'C'], index=tti(start='2023-01-01', end='2023-01-31', freq='w-fri',
                                                         time_offset="15:00")  # len = 3
)

trade_price_h_df = pd.DataFrame(
        trade_price_h_data, columns=['A', 'B', 'C'],
        index=tti(start='2023-01-10',
                  end='2023-01-31',
                  freq='h',
                  include_start_am=False,
                  include_start_pm=False)  # len = 40
)

trade_price_d_df = pd.DataFrame(
        trade_price_h_data[:25], columns=['A', 'B', 'C'],
        index=tti(start='2023-01-01', end='2023-02-14', freq='D', time_offset="15:00")  # len = 25
)

close_ref_df = pd.DataFrame(
        dtype_ref_data, columns=['ref'],
        index=tti(start='2023-01-01', end='2023-02-14', freq='D', time_offset="15:00")  # len = 25
)['ref']  # make this a pd.Series for reference types


# basic test strategies
class TestGenStg(GeneralStg):
    """用于Test测试的通用交易策略，基于GeneralStrategy策略生成

    策略参数：param1，param2
    历史数据：close_E_d：日线收盘价x4天，close_E_h：60分钟收盘价x5小时
    输出信号：各个股票的持仓比例
    策略逻辑：
    当历史数据不含参考数据和交易结果数据时：
     - 计算：N日变化率 = (今收-param1日前收)/今收，
     - 计算：N小时变化率 = (今收-param1小时前收)/今收
     - 计算：综合变化率 = N日变化率*param2 + N小时变化率*(1-param2)
     - 选择今日变化率最高的两支，设定投资比率50%，否则投资比例为0
    """

    def __init__(self, par_values: tuple = None, **kwargs):
        super().__init__(
                name='test_gen',
                description='test general strategy',
                pars=[param1.copy(), param2.copy()],
                data_types={'close_E_d': dtype_1, 'close_E_h': dtype_2},
                use_latest_data_cycle=[False, False],
                window_length=[4, 5],
                **kwargs,
        )

        self.param1 = None
        if par_values:
            self.update_par_values(*par_values)

    def realize(self):

        # p1, p2 = self.param1, self.param2
        p1, p2 = self.get_pars('param1'), self.get_pars('param2')
        # close_d, close_h = self.close_E_d, self.close_E_h
        close_d, close_h = self.get_data('close_E_d'), self.get_data('close_E_h')
        print("GeneralStg is running")
        print(f"param1 = {p1}, param2 = {p2}")
        print(f"got datas:\n{close_d}\n and \n{close_h}")
        # create signal according to class doc and print out step results
        dt1_change = (close_d[-1] - close_d[-p1]) / close_d[-1]
        dt2_change = (close_h[-1] - close_h[-p1]) / close_h[-1]
        print(f"dt1_change = (close_d[-1] - close_d[-{p1}]) / close_d[-1] = \n{dt1_change}")
        print(f"dt2_change = (close_h[-1] - close_h[-{p1}]) / close_h[-1] = \n{dt2_change}")
        combined_change = dt1_change * p2 + dt2_change * (1 - p2)
        print(f"combined_change = dt1_change * {p2} + dt2_change * (1 - {p2}) = \n{combined_change}")
        # get top 2 stock indexes
        top2_idx = combined_change.argsort()[-2:][::-1]
        print(f"top2 indexes = {top2_idx}")
        signal = np.zeros_like(combined_change)
        signal[top2_idx] = 0.5
        print(f"signal = {signal}")

        return signal


class TestFactorSorter(FactorSorter):
    """用于Test测试的简单选股策略，基于FactorSorter策略生成

    策略参数：param1，param2
    历史数据：close_E_d：日线收盘价x3天，close_E_w：周收盘价x1周
    输出信号：各个股票的持仓比例
    策略逻辑：
    当历史数据不含参考数据和交易结果数据时：
     - 计算：N日平均价 = param1日内收盘平均价，
     - 计算：周价变化率 = (今收-param1小时前收)/本周收盘价
     - 计算：选股因子 = N日平均价*param2 + 周价变化率*(1-param2)
     - 将所有股票的选股因子排序后根据FactorSorter策略的标准选股方法进行选股
    """

    def __init__(self, par_values=None, **kwargs):
        super().__init__(
                name='test_factor_sorter',
                description='test factor sorter strategy',
                pars=[param1.copy(), param2.copy()],
                data_types={'close_E_d': dtype_1, 'close_E_w': dtype_5},
                use_latest_data_cycle=[True, False],
                window_length=[3, 1],
                **kwargs,
        )

        if par_values:
            self.update_par_values(*par_values)

    def realize(self):

        # p1, p2 = self.param1, self.param2
        p1, p2 = self.get_pars('param1'), self.get_pars('param2')
        # close_d, close_w = self.close_E_d, self.close_E_w
        close_d, close_w = self.get_data('close_E_d'), self.get_data('close_E_w')
        print("FactorSorter is running")
        print(f"param1 = {p1}, param2 = {p2}")
        print(f"got datas:\n{close_d}\n and \n{close_w}")
        # create signal according to class doc and print out step results
        dt1_change = np.mean(close_d[-p1:], axis=0)
        dt2_change = (close_d[-1] - close_d[-p1]) / close_w[-1]
        print(f"dt1_avg = mean(close_d[-{p1}:], axis=0) = \n{dt1_change}")
        print(f"dt2_change = (close_d[-1] - close_d[-{p1}]) / close_w[-1] = \n{dt2_change}")
        selection_factor = dt1_change * p2 + dt2_change * (1 - p2)
        print(f"selection_factor = dt1_avg * {p2} + dt2_change * (1 - {p2}) = \n{selection_factor}")

        return selection_factor


class TestRuleIter(RuleIterator):
    """用于Test测试的循环规则策略，基于RuleIterator策略生成

    策略参数：param3，param4 (4-1, 4-2, 4-3)
    历史数据：close_E_h：小时收盘价x5小时，close_E_15min：15分钟收盘价x20周期
    输出信号：各个股票的持仓比例
    策略逻辑：
    对股票池中的每只股票执行下面的操作：
    - 计算：小时线均价 = param4-1 小时收盘价均值
    - 计算：15分钟线均价1 = param4-2周期收盘价均值
    - 计算：15分钟线均价2 = param4-3周期收盘价均值
    - 当param3参数分别为以下option时，输出信号不同：
        - option 1：当小时线均价 >= 15分钟线均价1 - 15分钟线均价2时，输出1，否则输出0
        - option 2：当小时线均价 <= 15分钟线均价1 - 15分钟线均价2时，输出1，否则输出0
        - option 3：当小时线均价介于15分钟线均价1和15分钟线均价2之间时，输出0，否则输出1
    """

    def __init__(self, par_values=None, **kwargs):
        super().__init__(
                name='test_rule_iterator',
                description='test rule iterator strategy',
                pars=[param3.copy(), param4.copy()],
                data_types={'close_E_h': dtype_2, 'close_E_15min': dtype_4},
                use_latest_data_cycle=[True, False],
                window_length=[5, 20],
                **kwargs,
        )

        if par_values:
            self.update_par_values(*par_values)

    def realize(self):

        # p1, p2 = self.param3, self.param4
        p1, p2 = self.get_pars('param3'), self.get_pars('param4')
        # close_h, close_m = self.close_E_h, self.close_E_15min
        close_h, close_m = self.get_data('close_E_h'), self.get_data('close_E_15min')
        print("RuleIterator is running")
        print(f"param3 = {p1}, param4 = {p2}")
        print(f"got datas:\n{close_h}\n and \n{close_m}")
        # create signal according to class doc and print out step results
        h_avg = np.mean(close_h[-p2[0]:], axis=0)
        m1_avg = np.mean(close_m[-p2[1]:], axis=0)
        m2_avg = np.mean(close_m[-p2[2]:], axis=0)
        print(f"h_avg = mean(close_h[-{p2[0]}:], axis=0) = {h_avg}")
        print(f"m1_avg = mean(close_m[-{p2[1]}:], axis=0) = {m1_avg}")
        print(f"m2_avg = mean(close_m[-{p2[2]}:], axis=0) = {m2_avg}")
        if p1 == 'option1':
            signal = 0.333 if h_avg >= ((m1_avg + m2_avg) / 2) else 0
            print(f"option1: signal = 1 if h_avg({h_avg}) >= avg(m1_avg, m2_avg)"
                  f"({((m1_avg + m2_avg) / 2)}) else 0")
        elif p1 == 'option2':
            signal = 0.333 if h_avg < ((m1_avg + m2_avg) / 2) else 0
            print(f"option2: signal = 1 if h_avg({h_avg}) <= avg(m1_avg, m2_avg)"
                  f"({((m1_avg + m2_avg) / 2)}) else 0")
        else:  # p1 == 'option3':
            signal = 0 if m2_avg <= m1_avg else 0.333
            print(f"option3: signal = 0 if m2_avg({m2_avg}) <= m1_avg({m1_avg}) else 1")
        print(f"signal = {signal}")
        return signal


class TestReferenceData(GeneralStg):
    """用于Test测试的参考数据策略，基于GeneralStrategy策略生成

    策略参数：param1(N): 股票变化率周期N，param2(M): 参考数据变化率周期
    历史数据：close_E_d：小时收盘价x5天，close:000300.SH_IDX_d：参考数据指数收盘价格x7天
    输出信号：各个股票的持仓比例
    策略逻辑：
     - 计算：股票N日变化率 = (今收-N日前收)/今收
     - 计算：参考数据M日变化率 = (今收-M小时前价)/今收
     - 判断：
        - 当参考数据M日变化率 > 0时：选择今日变化率最高的两支，设定投资比率50%，否则投资比例为0
        - 当参考数据M日变化率 <= 0时：选择今日变化率最低的两支，设定投资比率50%，否则投资比例为0
    """

    def __init__(self, par_values: tuple = None, **kwargs):
        super().__init__(
                name='test_ref_data',
                description='test reference data strategy',
                pars=[
                    Parameter((1, 5), name='N', par_type='int', value=5),
                    Parameter((1, 7), name='M', par_type='int', value=7),
                ],
                data_types={'close_E_d': dtype_1, 'reference_d': dtype_ref},
                use_latest_data_cycle=[False, False],
                window_length=[5, 7],
                **kwargs,
        )
        if par_values:
            self.update_par_values(*par_values)

    def realize(self):
        """实现策略逻辑"""

        # n, m = self.param1, self.param2
        n, m = self.get_pars('N'), self.get_pars('M')
        # close_d, ref_d = self.close_E_d, self.reference_d
        close_d, ref_d = self.get_data('close_E_d'), self.get_data('reference_d')
        # print("ReferenceData is running")
        # print(f"param1(N) = {n}, param2(M) = {m}")
        # print(f"got datas:\n{close_d}\n and \n{ref_d}")
        # create signal according to class doc and print out step results
        dt1_change = (close_d[-1] - close_d[-n]) / close_d[-1]
        ref_change = (ref_d[-1] - ref_d[-m]) / ref_d[-1]
        # print(f"dt1_change = (close_d[-1] - close_d[-{n}]) / close_d[-1] = \n{dt1_change}")
        # print(f"ref_change = (ref_d[-1] - ref_d[-{m}]) / ref_d[-1] = \n{ref_change}")
        # get top 2 stock indexes
        if ref_change > 0:
            top2_idx = dt1_change.argsort()[-2:][::-1]
            # print(f"ref_change > 0, top2 indexes = {top2_idx}")
        else:
            top2_idx = dt1_change.argsort()[:2]
            # print(f"ref_change <= 0, bottom2 indexes = {top2_idx}")
        signal = np.zeros_like(dt1_change)
        signal[top2_idx] = 0.5
        # print(f"signal = {signal}")

        return signal


class TestDependentData(GeneralStg):
    """用于Test测试的依赖性数据策略，基于GeneralStrategy策略生成

        策略参数：param1(N): 股票变化率周期N，param2(M): 参考数据变化率周期
        历史数据：close_E_d：小时收盘价x5天，trade_price：依赖性数据，上次交易价格x1天
        输出信号：各个股票的持仓比例
        策略逻辑：
         - 计算：股票N日变化率 = (今收-N日前收)/今收
         - 计算：当前价格胜率 = (今收-交易价格)/今收
         - 判断：
            - 当当前价格胜率 > 0时：选择今日变化率最高的两支，设定投资比率50%，否则投资比例为0
            - 当当前价格胜率 <= 0时：选择今日变化率最低的两支，设定投资比率50%，否则投资比例为0
        """

    def __init__(self, par_values: tuple = None, **kwargs):
        super().__init__(
                name='test_ref_data',
                description='test reference data strategy',
                pars=[
                    Parameter((1, 5), name='N', par_type='int', value=5),
                    Parameter((1, 7), name='M', par_type='int', value=7),
                ],
                data_types={'close_E_d':         dtype_1,
                            'reference_d':       dtype_ref,
                            'holding_positions': DataType('op_holding_positions'),
                            'trade_prices':      DataType('op_trade_prices'),
                            'op_cashes':         DataType('op_cashes'),
                            'op_trade_volumes':  DataType('op_trade_volumes')},
                use_latest_data_cycle=[False, False],
                window_length=[5, 7],
                **kwargs,
        )
        if par_values:
            self.update_par_values(*par_values)

    def realize(self):
        """实现策略逻辑，在策略中需要用到holding和trade_price两种数据， singaltype = VS"""
        # n, m = self.param1, self.param2
        n, m = self.get_pars('N'), self.get_pars('M')
        close_d, ref_d = self.get_data('close_E_d'), self.get_data('reference_d')
        # print("ReferenceData is running")
        # print(f"param1(N) = {n}, param2(M) = {m}")
        # print(f"got datas:\n{close_d}\n and \n{ref_d}")
        # create signal according to class doc and print out step results
        dt1_change = (close_d[-1] - close_d[-n]) / close_d[-1]
        ref_change = (ref_d[-1] - ref_d[-m]) / ref_d[-1]
        # print(f"dt1_change = (close_d[-1] - close_d[-{n}]) / close_d[-1] = \n{dt1_change}")
        # print(f"ref_change = (ref_d[-1] - ref_d[-{m}]) / ref_d[-1] = \n{ref_change}")
        # get top 2 stock indexes
        if ref_change > 0:
            top2_idx = dt1_change.argsort()[-2:][::-1]
            # print(f"ref_change > 0, top2 indexes = {top2_idx}")
        else:
            top2_idx = dt1_change.argsort()[:2]
            # print(f"ref_change <= 0, bottom2 indexes = {top2_idx}")
        signal = np.zeros_like(dt1_change)
        signal[top2_idx] = 0.5  # 再买入
        # print(f"signal = {signal}")
        holding, prices = self.get_data('holding_positions', 'trade_prices')
        cashes, trade_volumes = self.get_data('op_cashes', 'op_trade_volumes')
        # print(f'got holding data:\n{holding}\n and trade prices:\n{prices}')
        # holding 和 prices 只是导入，做一些没有意义的计算，策略
        # 输出结果与TestReferenceData是一样的
        # print(f'got position holdings: {holding}')
        # print(f'got trade prices:  {prices}')
        # print(f'got cashes: {cashes}')
        # print(f'got trade volumes: {trade_volumes}')

        return signal


class TestOperatorAndStrategy(unittest.TestCase):
    """全面测试Operator对象的所有功能。包括：

        1, Strategy 参数的设置
        2, 历史数据的获取与分配提取
        3, 策略优化参数的批量设置和优化空间的获取
        4, 策略输出值的正确性验证
        5, 策略结果的混合结果确认
    """

    def setUp(self):
        """prepare arr for Operator test"""

        print('start testing HistoryPanel object\n')

        # 1， 准备模拟历史数据对象
        pass

    def test_init(self):
        """ test initialization of Operator class"""
        op = qt.Operator(name='test_operator')
        self.assertIsInstance(op, qt.Operator)
        self.assertIsInstance(op.strategies, list)
        self.assertEqual(len(op.strategies), 0)
        op = qt.Operator('dma')
        self.assertIsInstance(op, qt.Operator)
        self.assertIsInstance(op.strategies, list)
        self.assertIsInstance(op.strategies[0], DMA)
        op = qt.Operator('dma, macd')
        self.assertIsInstance(op, qt.Operator)
        op = qt.Operator(['dma', 'macd'])
        self.assertIsInstance(op, qt.Operator)

        # test init with other parameters like signal_type, run_freq, run_timing, group_merge_type
        op = qt.Operator('dma, macd', signal_type='ps')
        self.assertEqual(op.groups['Group_1'].signal_type, 'ps')

        op = qt.Operator('dma, macd', run_freq='15min')
        self.assertEqual(op.groups['Group_1'].run_freq, '15min')
        self.assertEqual(op['dma'].run_freq, '15min')
        self.assertEqual(op['macd'].run_freq, '15min')
        self.assertEqual(op.groups['Group_1'].run_timing, 'close')
        self.assertRaises(TypeError, qt.Operator, 'dma, macd', run_freq=15)
        self.assertRaises(ValueError, qt.Operator, 'dma, macd', run_freq='5hourly')

        op = qt.Operator('dma, macd', run_timing='open')
        self.assertEqual(op.groups['Group_1'].run_timing, 'open')
        self.assertEqual(op['dma'].run_timing, 'open')
        self.assertEqual(op['macd'].run_timing, 'open')
        self.assertEqual(op.groups['Group_1'].run_freq, 'd')

        op = qt.Operator('dma, macd', group_merge_type='and')
        self.assertEqual(op.group_merge_type, 'And')
        self.assertRaises(TypeError, qt.Operator, 'dma, macd', group_merge_type=15)
        self.assertRaises(ValueError, qt.Operator, 'dma, macd', group_merge_type='wrong_type')

        op = qt.Operator('dma, macd', op_type='stepwise')
        self.assertEqual(op.op_type, 'stepwise')
        self.assertRaises(KeyError, qt.Operator, 'dma, macd', op_type=15)
        self.assertRaises(KeyError, qt.Operator, 'dma, macd', op_type='fast')

    def test_repr(self):
        """ test basic representation of Opeartor class"""
        op = qt.Operator()
        self.assertEqual(op.__repr__(), 'Operator([], name=\'None\')')

        op = qt.Operator('macd, dma, trix, random, ndayavg')
        self.assertEqual(op.__repr__(), 'Operator([macd, dma, trix, random, ndayavg], name=\'None\')')
        self.assertEqual(op['dma'].__repr__(), 'RULE-ITER(DMA, (12, 26, 9))')
        self.assertEqual(op['macd'].__repr__(), 'RULE-ITER(MACD, (12, 26, 9))')
        self.assertEqual(op['trix'].__repr__(), 'RULE-ITER(TRIX, (12, 12))')
        self.assertEqual(op['random'].__repr__(), 'GENERAL(RANDOM, (0.5,))')
        self.assertEqual(op['ndayavg'].__repr__(), 'FACTOR(N-DAY AVG, (14,))')

    def test_get_next_ids(self):
        """ test functions _next_stg_id and _next_group_id with fake ids"""
        op = qt.Operator()
        self.assertEqual(op.strategy_ids, [])

        # test getting next id in simple cases
        op.add_strategies('dma, macd, trix')
        self.assertEqual(op._next_stg_id('dma'), 'dma_1')
        self.assertEqual(op._next_stg_id('macd'), 'macd_1')
        self.assertEqual(op._next_stg_id('trix'), 'trix_1')
        self.assertEqual(op._next_stg_id('random'), 'random')

        # test getting next id in missing indexes
        op.add_strategies('dma, dma, dma')
        self.assertEqual(op._next_stg_id('dma'), 'dma_4')
        self.assertEqual(op._next_stg_id('macd'), 'macd_1')

        # test getting next group id in simple cases
        op = qt.Operator()
        self.assertEqual(op.group_ids, [])
        self.assertEqual(op._next_group_id(), 'Group_1')

        # test getting next group id in missing indexes
        op._groups = [Group('Group_1', run_freq='d', run_timing='close'),
                      Group('Group_3', run_freq='d', run_timing='close'),
                      Group('Group_5', run_freq='d', run_timing='close')]
        self.assertEqual(op._next_group_id(), 'Group_6')

    def test_operator_add_strategy(self):
        """test adding strategies to Operator"""
        op = qt.Operator('dma, all, sellrate')

        self.assertIsInstance(op, qt.Operator)
        self.assertIsInstance(op.strategies[0], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[1], qt.built_in.SelectingAll)
        self.assertIsInstance(op.strategies[2], qt.built_in.SellRate)
        self.assertIsInstance(op[0], qt.built_in.DMA)
        self.assertIsInstance(op[1], qt.built_in.SelectingAll)
        self.assertIsInstance(op[2], qt.built_in.SellRate)
        self.assertIsInstance(op['dma'], qt.built_in.DMA)
        self.assertIsInstance(op['all'], qt.built_in.SelectingAll)
        self.assertIsInstance(op['sellrate'], qt.built_in.SellRate)
        self.assertEqual(op.strategy_count, 3)
        self.assertEqual(op.strategy_ids, ['dma', 'all', 'sellrate'])
        print(f'test adding strategies into existing op')
        print('test adding strategy by string')
        op.add_strategy('macd')
        self.assertIsInstance(op.strategies[0], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[3], qt.built_in.MACD)
        self.assertEqual(op.strategy_count, 4)
        op.add_strategy('random')
        self.assertIsInstance(op.strategies[0], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[4], qt.built_in.SelectingRandom)
        self.assertEqual(op.strategy_count, 5)
        test_ls = TestGenStg()
        op.add_strategy(test_ls)
        self.assertIsInstance(op.strategies[0], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[5], TestGenStg)
        self.assertEqual(op.strategy_count, 6)
        print(f'Test different instance of objects are added to operator')
        op.add_strategy('dma')
        self.assertIsInstance(op.strategies[0], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[6], qt.built_in.DMA)
        self.assertIsNot(op.strategies[0], op.strategies[6])
        print(f'Test adding strategy with class name')
        op.add_strategy(TestRuleIter)
        self.assertIsInstance(op.strategies[0], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[7], TestRuleIter)
        self.assertEqual(op.strategy_count, 8)
        self.assertEqual(op.strategy_ids,
                         ['dma', 'all', 'sellrate', 'macd', 'random', 'custom', 'dma_1', 'custom_1'])
        self.assertEqual(op.strategy_groups, {'Group_1': op._groups[0]})
        self.assertEqual(op.strategy_group_count, 1)

        print(f'create a new empty operator and add strategies with different run freq and run timing')
        op = qt.Operator()  # now operator has no strategy and no strategy group
        self.assertIsInstance(op, qt.Operator)
        self.assertEqual(op.strategy_count, 0)
        self.assertEqual(op.strategy_group_count, 0)
        self.assertEqual(op.strategy_ids, [])
        self.assertEqual(op.group_ids, [])

        print(f'add a strategy with default run freq and run timing')
        op.add_strategy(TestRuleIter(), run_freq='d', run_timing='close')
        self.assertIsInstance(op, qt.Operator)
        self.assertEqual(op.strategy_count, 1)
        self.assertEqual(op.strategy_group_count, 1)
        self.assertEqual(op.strategy_ids, ['custom'])
        self.assertEqual(op.group_ids, ['Group_1'])
        self.assertEqual(op.groups['Group_1'].run_freq, 'd')
        self.assertEqual(op.groups['Group_1'].run_timing, 'close')
        self.assertTrue(all(stg.run_freq == 'd' for stg in op.groups['Group_1'].members))
        self.assertTrue(all(stg.run_timing == 'close' for stg in op.groups['Group_1'].members))

        print(f'add a strategy with run freq h and run timing close')
        op.add_strategy(TestRuleIter(), run_freq='h', run_timing='close')
        self.assertIsInstance(op, qt.Operator)
        self.assertEqual(op.strategy_count, 2)
        self.assertEqual(op.strategy_group_count, 2)
        self.assertEqual(op.strategy_ids, ['custom', 'custom_1'])
        self.assertEqual(op.group_ids, ['Group_1', 'Group_2'])
        self.assertEqual(op.groups['Group_2'].run_freq, 'h')
        self.assertEqual(op.groups['Group_2'].run_timing, 'close')
        self.assertTrue(all(stg.run_freq == 'h' for stg in op.groups['Group_2'].members))
        self.assertTrue(all(stg.run_timing == 'close' for stg in op.groups['Group_2'].members))

        print(f'add a strategy again with run freq d and run timing close')
        op.add_strategy(TestRuleIter(), run_freq='d', run_timing='close')
        self.assertIsInstance(op, qt.Operator)
        self.assertEqual(op.strategy_count, 3)
        self.assertEqual(op.strategy_group_count, 2)
        self.assertEqual(op.strategy_ids, ['custom', 'custom_2', 'custom_1'])
        self.assertEqual(op.group_ids, ['Group_1', 'Group_2'])
        self.assertEqual(op.groups['Group_1'].run_freq, 'd')
        self.assertEqual(op.groups['Group_1'].run_timing, 'close')
        self.assertEqual(op.groups['Group_1'].strategy_count, 2)
        print(f'Group_1 has {op.groups["Group_1"].strategy_count} strategies')
        self.assertEqual(op.groups['Group_2'].strategy_count, 1)
        self.assertEqual(op.groups['Group_2'].run_freq, 'h')
        self.assertEqual(op.groups['Group_2'].run_timing, 'close')
        self.assertTrue(all(stg.run_freq == 'd' for stg in op.groups['Group_1'].members))
        self.assertTrue(all(stg.run_timing == 'close' for stg in op.groups['Group_1'].members))
        self.assertTrue(all(stg.run_freq == 'h' for stg in op.groups['Group_2'].members))
        self.assertTrue(all(stg.run_timing == 'close' for stg in op.groups['Group_2'].members))

        print(f'test adding strategy with run freq h and run timing open')
        op.add_strategy(TestRuleIter(), run_freq='h', run_timing='open')
        self.assertIsInstance(op, qt.Operator)
        self.assertEqual(op.strategy_count, 4)
        self.assertEqual(op.strategy_group_count, 3)
        self.assertEqual(op.strategy_ids, ['custom', 'custom_2', 'custom_1', 'custom_3'])
        self.assertEqual(op.group_ids, ['Group_1', 'Group_2', 'Group_3'])
        self.assertEqual(op.groups['Group_3'].run_freq, 'h')
        self.assertEqual(op.groups['Group_3'].run_timing, 'open')
        self.assertEqual(op.groups['Group_3'].strategy_count, 1)
        print(f'Group_3 has {op.groups["Group_3"].strategy_count} strategies')
        self.assertTrue(all(stg.run_freq == 'h' for stg in op.groups['Group_3'].members))
        self.assertTrue(all(stg.run_timing == 'open' for stg in op.groups['Group_3'].members))
        print(f'test adding strategy with run freq d and run timing close')
        op.add_strategy(TestRuleIter(), run_freq='d', run_timing='close')
        self.assertIsInstance(op, qt.Operator)
        self.assertEqual(op.strategy_count, 5)
        self.assertEqual(op.strategy_group_count, 3)
        self.assertEqual(op.strategy_ids, ['custom', 'custom_2', 'custom_4', 'custom_1', 'custom_3'])
        self.assertEqual(op.group_ids, ['Group_1', 'Group_2', 'Group_3'])
        self.assertEqual(op.groups['Group_1'].run_freq, 'd')
        self.assertEqual(op.groups['Group_1'].run_timing, 'close')
        self.assertEqual(op.groups['Group_1'].strategy_count, 3)
        print(f'Group_1 has {op.groups["Group_1"].strategy_count} strategies')
        self.assertEqual(op.groups['Group_2'].strategy_count, 1)
        self.assertEqual(op.groups['Group_2'].run_freq, 'h')
        self.assertEqual(op.groups['Group_2'].run_timing, 'close')
        self.assertEqual(op.groups['Group_3'].strategy_count, 1)
        self.assertEqual(op.groups['Group_3'].run_freq, 'h')
        self.assertEqual(op.groups['Group_3'].run_timing, 'open')
        self.assertTrue(all(stg.run_freq == 'd' for stg in op.groups['Group_1'].members))
        self.assertTrue(all(stg.run_timing == 'close' for stg in op.groups['Group_1'].members))
        self.assertTrue(all(stg.run_freq == 'h' for stg in op.groups['Group_2'].members))
        self.assertTrue(all(stg.run_timing == 'close' for stg in op.groups['Group_2'].members))
        self.assertTrue(all(stg.run_freq == 'h' for stg in op.groups['Group_3'].members))
        self.assertTrue(all(stg.run_timing == 'open' for stg in op.groups['Group_3'].members))

    def test_operator_add_strategies(self):
        """ etst adding multiple strategies to Operator"""
        op = qt.Operator('dma, all, sellrate')
        self.assertEqual(op.strategy_count, 3)
        print('test adding multiple strategies -- adding strategy by list of strings')
        op.add_strategies(['dma', 'macd'])
        self.assertEqual(op.strategy_count, 5)
        self.assertIsInstance(op.strategies[0], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[3], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[4], qt.built_in.MACD)
        print('test adding multiple strategies -- adding strategy by comma separated strings')
        op.add_strategies('dma, macd')
        self.assertEqual(op.strategy_count, 7)
        self.assertIsInstance(op.strategies[0], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[5], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[6], qt.built_in.MACD)
        print('test adding multiple strategies -- adding strategy by list of strategies')
        op.add_strategies([qt.built_in.DMA(), qt.built_in.MACD()])
        self.assertEqual(op.strategy_count, 9)
        self.assertIsInstance(op.strategies[0], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[7], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[8], qt.built_in.MACD)
        print('test adding multiple strategies -- adding strategy by list of strategy and str')
        op.add_strategies(['DMA', qt.built_in.MACD()])
        self.assertEqual(op.strategy_count, 11)
        self.assertIsInstance(op.strategies[0], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[9], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[10], qt.built_in.MACD)
        self.assertIsNot(op.strategies[0], op.strategies[9])
        self.assertIs(type(op.strategies[0]), type(op.strategies[9]))
        print('test adding multiple strategies -- adding strategy by list of custom strategy and custom strategy names')
        op.add_strategies([TestGenStg, TestGenStg(), TestRuleIter()])
        self.assertEqual(op.strategy_count, 14)
        self.assertIsInstance(op.strategies[0], qt.built_in.DMA)
        self.assertIsInstance(op.strategies[11], TestGenStg)
        self.assertIsInstance(op.strategies[12], TestGenStg)
        self.assertIsInstance(op.strategies[13], TestRuleIter)
        self.assertIsNot(op.strategies[11], op.strategies[12])
        self.assertIsNot(op.strategies[11], op.strategies[13])
        self.assertIsNot(op.strategies[12], op.strategies[13])
        self.assertEqual(op.strategy_group_count, 1)
        self.assertEqual(op.groups, {'Group_1': op._groups[0]})
        print('test adding multiple strategies with different run freq and run timing')
        op.add_strategy(TestRuleIter(), run_freq='d', run_timing='close')
        op.add_strategy(TestRuleIter(), run_freq='h', run_timing='close')
        op.add_strategy(TestRuleIter(), run_freq='h', run_timing='open')
        self.assertEqual(op.strategy_count, 17)
        self.assertEqual(op.strategy_group_count, 3)
        self.assertEqual(op.strategy_ids, ['dma', 'all', 'sellrate', 'dma_1', 'macd', 'dma_2', 'macd_1',
                                           'dma_3', 'macd_2', 'dma_4', 'macd_3', 'custom', 'custom_1',
                                           'custom_2', 'custom_3', 'custom_4', 'custom_5'])
        self.assertEqual(op.group_ids, ['Group_1', 'Group_2', 'Group_3'])
        self.assertEqual(op.groups['Group_1'].run_freq, 'd')
        self.assertEqual(op.groups['Group_1'].run_timing, 'close')
        self.assertEqual(op.groups['Group_1'].strategy_count, 15)
        print(f'Group_1 has {op.groups["Group_1"].strategy_count} strategies')
        self.assertEqual(op.groups['Group_2'].run_freq, 'h')
        self.assertEqual(op.groups['Group_2'].run_timing, 'close')
        self.assertEqual(op.groups['Group_2'].strategy_count, 1)
        print(f'Group_2 has {op.groups["Group_2"].strategy_count} strategies')
        self.assertEqual(op.groups['Group_3'].run_freq, 'h')
        self.assertEqual(op.groups['Group_3'].run_timing, 'open')
        self.assertEqual(op.groups['Group_3'].strategy_count, 1)
        print(f'Group_3 has {op.groups["Group_3"].strategy_count} strategies')

        print(f'test adding multiple strategies with common run_timing and run_freq parameters')
        op.add_strategies([TestRuleIter, 'dma', TestGenStg], run_timing='open', run_freq='d')
        op.add_strategies(['macd', TestGenStg()], run_timing='close', run_freq='h')
        self.assertEqual(op.strategy_count, 22)
        self.assertEqual(op.strategy_group_count, 4)
        self.assertEqual(op.strategy_ids, [
            'dma', 'all', 'sellrate', 'dma_1', 'macd', 'dma_2', 'macd_1',
            'dma_3', 'macd_2', 'dma_4', 'macd_3', 'custom', 'custom_1',
            'custom_2', 'custom_3', 'custom_4', 'macd_4', 'custom_8',
            'custom_5', 'custom_6', 'dma_5', 'custom_7',
        ])
        self.assertEqual(op.group_ids, ['Group_1', 'Group_2', 'Group_3', 'Group_4'])
        self.assertEqual(op.groups['Group_1'].run_freq, 'd')
        self.assertEqual(op.groups['Group_1'].run_timing, 'close')
        self.assertEqual(op.groups['Group_1'].strategy_count, 15)
        self.assertEqual(op.get_strategy_id_by_group('Group_1'), ['dma',
                                                                  'all',
                                                                  'sellrate',
                                                                  'dma_1',
                                                                  'macd',
                                                                  'dma_2',
                                                                  'macd_1',
                                                                  'dma_3',
                                                                  'macd_2',
                                                                  'dma_4',
                                                                  'macd_3',
                                                                  'custom',
                                                                  'custom_1',
                                                                  'custom_2',
                                                                  'custom_3'])
        self.assertEqual(op.get_strategy_count_by_group('Group_1'), 15)
        print(f'Group_1 has {op.groups["Group_1"].strategy_count} strategies')
        self.assertEqual(op.groups['Group_2'].run_freq, 'h')
        self.assertEqual(op.groups['Group_2'].run_timing, 'close')
        self.assertEqual(op.groups['Group_2'].strategy_count, 3)
        self.assertEqual(op.get_strategy_id_by_group('Group_2'), ['custom_4', 'macd_4', 'custom_8'])
        self.assertEqual(op.get_strategy_count_by_group('Group_2'), 3)
        print(f'Group_2 has {op.groups["Group_2"].strategy_count} strategies')
        self.assertEqual(op.groups['Group_3'].run_freq, 'h')
        self.assertEqual(op.groups['Group_3'].run_timing, 'open')
        self.assertEqual(op.groups['Group_3'].strategy_count, 1)
        self.assertEqual(op.get_strategy_id_by_group('Group_3'), ['custom_5'])
        self.assertEqual(op.get_strategy_count_by_group('Group_3'), 1)
        print(f'Group_3 has {op.groups["Group_3"].strategy_count} strategies')
        self.assertEqual(op.groups['Group_4'].run_freq, 'd')
        self.assertEqual(op.groups['Group_4'].run_timing, 'open')
        self.assertEqual(op.groups['Group_4'].strategy_count, 3)
        self.assertEqual(op.get_strategy_id_by_group('Group_4'), ['custom_6', 'dma_5', 'custom_7'])
        self.assertEqual(op.get_strategy_count_by_group('Group_4'), 3)
        print(f'Group_4 has {op.groups["Group_4"].strategy_count}')

        print('test adding fault arr')
        self.assertRaises(AssertionError, op.add_strategies, 123)
        self.assertRaises(AssertionError, op.add_strategies, None)

    def test_operator_remove_strategy(self):
        """ test method remove strategy"""

        print(f'create a new operator with 7 strategies in 3 groups')
        op = qt.Operator('dma, all, sellrate')
        op.add_strategies(['dma', 'macd'], run_freq='h', run_timing='close')
        op.add_strategies(['DMA', TestGenStg()], run_freq='h', run_timing='open')
        self.assertEqual(op.strategy_count, 7)
        self.assertEqual(op.strategy_ids, ['dma', 'all', 'sellrate', 'dma_1', 'macd', 'dma_2', 'custom'])
        self.assertEqual(op.strategy_group_count, 3)
        self.assertEqual(op.strategy_groups, {'Group_1': op._groups[0],
                                              'Group_2': op._groups[1],
                                              'Group_3': op._groups[2]}
                         )
        self.assertEqual(op.group_ids, ['Group_1', 'Group_2', 'Group_3'])
        self.assertEqual(op.groups['Group_1'].strategy_count, 3)
        self.assertEqual(op.groups['Group_1'].members, [op['dma'], op['all'], op['sellrate']])
        self.assertEqual(op.groups['Group_2'].strategy_count, 2)
        self.assertEqual(op.groups['Group_2'].members, [op['dma_1'], op['macd']])
        self.assertEqual(op.groups['Group_3'].strategy_count, 2)
        self.assertEqual(op.groups['Group_3'].members, [op['dma_2'], op['custom']])
        print('test removing strategies from Operator')
        op.remove_strategy('dma')
        self.assertEqual(op.strategy_count, 6)
        self.assertEqual(op.strategy_ids, ['all', 'sellrate', 'dma_1', 'macd', 'dma_2', 'custom'])
        self.assertEqual(op.strategies[0], op['all'])
        self.assertEqual(op.strategies[1], op['sellrate'])
        self.assertEqual(op.strategies[2], op['dma_1'])
        self.assertEqual(op.strategies[3], op['macd'])
        self.assertEqual(op.strategies[4], op['dma_2'])
        self.assertEqual(op.strategies[5], op['custom'])
        self.assertEqual(op.strategy_group_count, 3)
        self.assertEqual(op.groups, {'Group_1': op._groups[0],
                                     'Group_2': op._groups[1],
                                     'Group_3': op._groups[2]})
        self.assertEqual(op.group_ids, ['Group_1', 'Group_2', 'Group_3'])
        self.assertEqual(op.groups['Group_1'].strategy_count, 2)
        self.assertEqual(op.groups['Group_1'].members, [op['all'], op['sellrate']])
        self.assertEqual(op.groups['Group_2'].strategy_count, 2)
        self.assertEqual(op.groups['Group_2'].members, [op['dma_1'], op['macd']])
        self.assertEqual(op.groups['Group_3'].strategy_count, 2)
        self.assertEqual(op.groups['Group_3'].members, [op['dma_2'], op['custom']])

        print(f'testing remove both strategies from group2')
        op.remove_strategy('dma_1')
        op.remove_strategy('macd')
        self.assertEqual(op.strategy_count, 4)
        self.assertEqual(op.strategy_ids, ['all', 'sellrate', 'dma_2', 'custom'])
        self.assertEqual(op.strategies[0], op['all'])
        self.assertEqual(op.strategies[1], op['sellrate'])
        self.assertEqual(op.strategies[2], op['dma_2'])
        self.assertEqual(op.strategies[3], op['custom'])
        self.assertEqual(op.strategy_group_count, 2)
        self.assertEqual(op.groups, {'Group_1': op._groups[0],
                                     'Group_3': op._groups[1]})
        self.assertEqual(op.group_ids, ['Group_1', 'Group_3'])
        self.assertEqual(op.groups['Group_1'].strategy_count, 2)
        self.assertEqual(op.groups['Group_1'].members, [op['all'], op['sellrate']])
        self.assertEqual(op.groups['Group_3'].strategy_count, 2)
        self.assertEqual(op.groups['Group_3'].members, [op['dma_2'], op['custom']])

        print(f'testing add strategies back to operator originally in group2, now they should be in group4')
        op.add_strategies(['dma', 'macd'], run_freq='h', run_timing='close')
        self.assertEqual(op.strategy_count, 6)
        self.assertEqual(op.strategy_ids, ['all', 'sellrate', 'dma_2', 'custom', 'dma_3', 'macd'])
        self.assertEqual(op.strategies[0], op['all'])
        self.assertEqual(op.strategies[1], op['sellrate'])
        self.assertEqual(op.strategies[2], op['dma_2'])
        self.assertEqual(op.strategies[3], op['custom'])
        self.assertEqual(op.strategies[4], op['dma_3'])
        self.assertEqual(op.strategies[5], op['macd'])
        self.assertEqual(op.strategy_group_count, 3)
        self.assertEqual(op.groups, {'Group_1': op._groups[0],
                                     'Group_3': op._groups[1],
                                     'Group_4': op._groups[2]})
        self.assertEqual(op.group_ids, ['Group_1', 'Group_3', 'Group_4'])
        self.assertEqual(op.groups['Group_1'].strategy_count, 2)
        self.assertEqual(op.groups['Group_1'].members, [op['all'], op['sellrate']])
        self.assertEqual(op.groups['Group_3'].strategy_count, 2)
        self.assertEqual(op.groups['Group_3'].members, [op['dma_2'], op['custom']])
        self.assertEqual(op.groups['Group_4'].strategy_count, 2)
        self.assertEqual(op.groups['Group_4'].members, [op['dma_3'], op['macd']])

        # test removing strategy that does not exist
        print(f'test removing strategy that does not exist')
        self.assertRaises(ValueError, op.remove_strategy, 'not_exist')
        self.assertRaises(ValueError, op.remove_strategy, 'all_1')
        self.assertEqual(op.strategy_count, 6)
        self.assertEqual(op.strategy_ids, ['all', 'sellrate', 'dma_2', 'custom', 'dma_3', 'macd'])

        # test removing all strategies from operator
        print(f'test removing all strategies from operator')
        op.remove_strategy()  # remove the last strategy
        print(f'strategies after remove the last one: {op.strategies}')
        self.assertEqual(op.strategy_ids, ['all', 'sellrate', 'dma_2', 'custom', 'dma_3'])
        op.remove_strategy(0)  # remove the first strategy
        print(f'strategies after remove the first one: {op.strategies}')
        self.assertEqual(op.strategy_ids, ['sellrate', 'dma_2', 'custom', 'dma_3'])
        op.remove_strategy(-2)  # remove the 2nd last strategy
        print(f'strategies after remove the 2nt last one: {op.strategies}')
        self.assertEqual(op.strategy_ids, ['sellrate', 'dma_2', 'dma_3'])
        op.remove_strategy(999)  # remove the last strategy
        print(f'strategies after remove the last one: {op.strategies}')
        self.assertEqual(op.strategy_ids, ['sellrate', 'dma_2'])
        op.remove_strategy(-999)  # remove the first strategy
        print(f'strategies after remove the last one: {op.strategies}')
        self.assertEqual(op.strategy_ids, ['dma_2'])
        op.remove_strategy('dma_2')
        self.assertEqual(op.strategy_count, 0)
        self.assertEqual(op.strategy_ids, [])
        self.assertEqual(op.strategies, [])
        self.assertEqual(op.strategy_group_count, 0)
        self.assertEqual(op.group_ids, [])
        self.assertEqual(op.strategy_group_count, 0)

        self.assertRaises(IndexError, op.remove_strategy, 0)

        op.add_strategy('dma')
        self.assertRaises(TypeError, op.remove_strategy, 35.76)

    def test_operator_clear_strategies(self):
        """ test operator clear strategies"""
        op = qt.Operator('dma, all, sellrate')
        op.add_strategies(['dma', 'macd'], run_freq='h', run_timing='close')
        op.add_strategies(['DMA', TestGenStg()], run_freq='h', run_timing='open')
        self.assertEqual(op.strategy_count, 7)
        self.assertEqual(op.strategy_ids, ['dma', 'all', 'sellrate', 'dma_1', 'macd', 'dma_2', 'custom'])
        self.assertEqual(op.strategy_group_count, 3)
        self.assertEqual(op.strategy_groups, {'Group_1': op._groups[0],
                                              'Group_2': op._groups[1],
                                              'Group_3': op._groups[2]}
                         )
        print('test removing strategies from Operator')
        op.clear_strategies()
        self.assertEqual(op.strategy_count, 0)
        self.assertEqual(op.strategy_ids, [])
        self.assertEqual(op.strategy_group_count, 0)
        self.assertEqual(op.strategy_groups, {})

        op.add_strategy('dma', par_values=(12, 123, 25), run_freq='5min', run_timing='open')
        self.assertEqual(op.strategy_count, 1)
        self.assertEqual(op.strategy_ids, ['dma'])
        self.assertEqual(type(op.strategies[0]), DMA)
        self.assertEqual(op.strategies[0].par_values, (12, 123, 25))
        self.assertEqual(op.strategy_group_count, 1)
        self.assertEqual(op.group_ids, ['Group_1'])
        self.assertEqual(op.groups, {'Group_1': op._groups[0]})
        self.assertEqual(op.groups['Group_1'].strategy_count, 1)
        self.assertEqual(op.groups['Group_1'].members, [op['dma']])
        self.assertEqual(op.groups['Group_1'].run_freq, '5min')
        self.assertEqual(op.groups['Group_1'].run_timing, 'open')

        op.clear_strategies()
        self.assertEqual(op.strategy_count, 0)
        self.assertEqual(op.strategy_ids, [])
        self.assertEqual(op.strategy_group_count, 0)
        self.assertEqual(op.group_ids, [])

    def test_info(self):
        """Test information output of Operator"""
        op = qt.Operator('dma, macd, trix')
        stg = qt.built_in.SelectingNDayRateChange()  # factor sorter info
        print(f'\n  --==**  test printing information of strategies, in simple mode\n')
        op[0].info()
        print(f'\n  --==**  test printing information of strategies, in simple mode\n')
        stg.info()

        print(f'\n  --==**  test printing information of strategies, in verbose mode\n')
        op[0].info(verbose=True)
        print(f'\n  --==**  test printing information of strategies, in verbose mode\n')
        stg.info(verbose=True)

        print(f'\n  --==**  test printing information of operator object\n')
        op.info()

        op.add_strategies('dma, macd', run_freq='h', run_timing='close')
        op.add_strategies([TestRuleIter, TestGenStg, TestFactorSorter], run_freq='h', run_timing='open')
        print(f'\n  --==**  test printing information of operator in simple mode\n')

        op.info()
        print(f'\n  --==**  test printing information of operator in verbose mode\n')

        op.info(verbose=True)

    def test_update_par_values(self):
        """ 测试设置策略参数，使用update_par_values"""
        op = qt.Operator('dma, macd, trix')

        stg_dma = op[0]
        stg_macd = op[1]
        stg_trix = op[2]

        # test setting normal parameter values
        stg_dma.par_values = (10, 20, 30)
        self.assertEqual(stg_dma.par_values, (10, 20, 30))
        stg_macd.par_values = (10, 20, 30.)
        self.assertEqual(stg_macd.par_values, (10, 20, 30))
        stg_trix.update_par_values(10, 20)
        self.assertEqual(stg_trix.par_values, (10, 20))
        stg_dma.update_par_values(10, 20, 30)
        self.assertEqual(stg_dma.par_values, (10, 20, 30))
        # update par values with kwargs
        stg_macd.update_par_values(slow=10, fast=20, mid=30)
        self.assertEqual(stg_macd.par_values, (10, 20, 30))
        # update partial kwargs
        stg_macd.update_par_values(fast=25)
        self.assertEqual(stg_macd.par_values, (10, 25, 30))
        stg_macd.update_par_values(12, 13)
        self.assertEqual(stg_macd.par_values, (12, 13, 30))

        # test update_par_values while par_count = 0
        op_no_par = qt.Operator('all')  # 'all' has no pars
        op_no_par[0].update_par_values()

        # test errors
        self.assertRaises(TypeError, stg_dma.update_par_values, 'wrong', 'input', 'type')  # wrong input type
        self.assertRaises(ValueError, stg_dma.update_par_values, 10, 10, 10, 10)  # par count does not match
        self.assertRaises(ValueError, stg_dma.update_par_values, 10, 10, -10)  # par out of range
        self.assertRaises(ValueError, stg_dma.update_par_values, 10, 10.5, -10)  # par type not match
        self.assertRaises(ValueError, stg_dma.update_par_ranges)  # no args nor kwargs while par_count > 0
        self.assertRaises(KeyError, stg_dma.update_par_values, not_existed_par=10)  # kwarg that does not exist

        # test setting multi-parameters to RuleIterators
        op.add_strategy(TestRuleIter)

        stg_rule = op[3]
        self.assertIsInstance(stg_rule, TestRuleIter)
        self.assertEqual(stg_rule.par_values[0], 'option1')
        self.assertTrue(np.allclose(stg_rule.par_values[1], np.array([1., 2., 3.])))
        stg_rule.update_par_values('option2', np.array([2., 3., 4.]))
        self.assertEqual(stg_rule.par_values[0], 'option2')
        self.assertTrue(np.allclose(stg_rule.par_values[1], np.array([2., 3., 4.])))

        self.assertEqual(stg_rule.allow_multi_par, True)
        stg_rule.update_shares(share_names=['000001', '000002', '000003'])
        stg_rule.update_par_values(
                {'000001': ('option1', np.array([1, 4, 5])),
                 '000002': ('option2', np.array([2, 4, 5])),
                 '000003': ('option3', np.array([3, 4, 5]))}
        )
        self.assertEqual(stg_rule.multi_pars[0][0], 'option1')
        self.assertTrue(np.allclose(stg_rule.multi_pars[0][1], np.array([1., 4., 5.])))
        self.assertEqual(stg_rule.multi_pars[1][0], 'option2')
        self.assertTrue(np.allclose(stg_rule.multi_pars[1][1], np.array([2., 4., 5.])))
        self.assertEqual(stg_rule.multi_pars[2][0], 'option3')
        self.assertTrue(np.allclose(stg_rule.multi_pars[2][1], np.array([3., 4., 5.])))
        self.assertEqual(stg_rule.par_values[0], 'option1')
        self.assertTrue(np.allclose(stg_rule.par_values[1], np.array([1., 4., 5.])))

        # update multi-parameter will fail if allow_multi_par is False
        stg_rule.allow_multi_par = False
        self.assertRaises(ValueError, stg_rule.update_par_values,
                          {'000001': (10, 10, 30),
                           '000002': (20, 20, 50),
                           '000003': (30, 30, 70)})

        # other types of strategies don't support multi-parameter
        op.add_strategy(GeneralStg)
        stg_gen = op[4]

        self.assertIsInstance(stg_gen, GeneralStg)
        self.assertEqual(stg_gen.par_values, ())
        self.assertRaises(ValueError, stg_gen.update_par_values,
                          {'000001': (10, 10, 30),
                           '000002': (20, 20, 50),
                           '000003': (30, 30, 70)})

    def test_get_strategy_by_id(self):
        """ test get_strategy_by_id()"""
        op = qt.Operator()
        self.assertIsInstance(op, qt.Operator)
        self.assertEqual(op.strategy_count, 0)
        self.assertEqual(op.strategy_ids, [])

        op = qt.Operator('macd, dma, trix')
        self.assertEqual(op.strategy_ids, ['macd', 'dma', 'trix'])
        self.assertIs(op.get_strategy_by_id('macd'), op.strategies[0])
        self.assertIs(op.get_strategy_by_id(1), op.strategies[1])
        self.assertIs(op.get_strategy_by_id('trix'), op.strategies[2])

    def test_get_items(self):
        """ test method __getitem__(), it should be the same as geting strategies by stg_id"""
        op = qt.Operator()
        self.assertIsInstance(op, qt.Operator)
        self.assertEqual(op.strategy_count, 0)
        self.assertEqual(op.strategy_ids, [])

        op = qt.Operator('macd, dma, trix')
        self.assertEqual(op.strategy_ids, ['macd', 'dma', 'trix'])
        self.assertIs(op['macd'], op.strategies[0])
        self.assertIs(op['trix'], op.strategies[2])
        self.assertIs(op[1], op.strategies[1])
        self.assertIs(op[2], op.strategies[2])

        # test other forms of op.__getitem__
        self.assertIs(op.get_stg(1), op.strategies[1])
        self.assertIs(op.get_strategy_by_id('dma'), op.strategies[1])

        # test Errors:
        self.assertRaises(TypeError, op.__getitem__, {})
        self.assertRaises(KeyError, op.__getitem__, 'not_existed_id')
        self.assertRaises(KeyError, op.__getitem__, -1)
        self.assertRaises(KeyError, op.__getitem__, 999)

    def test_get_group(self):
        """ test method get_group and related methods"""
        op = qt.Operator('dma, macd')
        op.add_strategies('trix, bband', run_timing='open')
        # there should be two groups in op

        self.assertEqual(op.strategy_group_count, 2)
        self.assertEqual(op.get_group(0), op.groups['Group_1'])
        self.assertEqual(op.get_group('Group_2'), op.groups['Group_2'])
        self.assertEqual(op.get_group_by_id(0), op.groups['Group_1'])
        self.assertEqual(op.get_group_by_id('Group_2'), op.groups['Group_2'])

        self.assertRaises(IndexError, op.get_group, 999)
        self.assertRaises(KeyError, op.get_group_by_id, 'Not_existed')

    def test_get_strategies_by_group(self):
        """ test get_strategies_by_price_type"""
        op = qt.Operator()
        self.assertIsInstance(op, qt.Operator)
        self.assertEqual(op.strategy_count, 0)
        self.assertEqual(op.strategy_ids, [])

        op.add_strategies('dma, macd, trix', run_timing='open', run_freq='h')
        op.add_strategies('dma, macd, trix', run_timing='close', run_freq='h')
        op.add_strategies('dma, macd, trix', run_timing='close', run_freq='d')
        self.assertEqual(op.strategy_group_count, 3)
        self.assertEqual(op.group_names, ['Group_1', 'Group_2', 'Group_3'])
        self.assertEqual(op.group_ids, ['Group_1', 'Group_2', 'Group_3'])
        self.assertEqual(op.strategy_groups, {'Group_1': op._groups[0],
                                              'Group_2': op._groups[1],
                                              'Group_3': op._groups[2]}
                         )

        stg_group1 = op.get_strategies_by_group('Group_1')
        stg_group2 = op.get_strategies_by_group('Group_2')
        stg_group3 = op.get_strategies_by_group('Group_3')

        self.assertIsInstance(stg_group1, list)
        self.assertIsInstance(stg_group2, list)
        self.assertIsInstance(stg_group3, list)

        self.assertEqual(stg_group1, [op['dma'], op['macd'], op['trix']])
        self.assertEqual(stg_group2, [op['dma_1'], op['macd_1'], op['trix_1']])
        self.assertEqual(stg_group3, [op['dma_2'], op['macd_2'], op['trix_2']])

        # test get strategies by passing wrong group name
        self.assertRaises(KeyError, op.get_strategies_by_group, 'not_exist')
        self.assertRaises(KeyError, op.get_strategies_by_group, 'Group_4')

        # test getting other strategy information by group
        self.assertEqual(op.get_strategy_count_by_group('Group_1'), 3)
        self.assertEqual(op.get_strategy_count_by_group('Group_2'), 3)
        self.assertEqual(op.get_strategy_count_by_group('Group_3'), 3)

        self.assertEqual(op.get_strategy_names_by_group('Group_1'), ['DMA', 'MACD', 'TRIX'])
        self.assertEqual(op.get_strategy_names_by_group('Group_2'), ['DMA', 'MACD', 'TRIX'])
        self.assertEqual(op.get_strategy_names_by_group('Group_3'), ['DMA', 'MACD', 'TRIX'])

        self.assertEqual(op.get_strategy_id_by_group('Group_1'), ['dma', 'macd', 'trix'])
        self.assertEqual(op.get_strategy_id_by_group('Group_2'), ['dma_1', 'macd_1', 'trix_1'])
        self.assertEqual(op.get_strategy_id_by_group('Group_3'), ['dma_2', 'macd_2', 'trix_2'])

    def test_property_strategies(self):
        """ test property strategies"""
        print(f'created a new simple Operator with only one strategy: DMA')
        op = qt.Operator('dma')
        strategies = op.strategies
        self.assertIsInstance(strategies, list)
        op.info()

        print(f'created the second simple Operator with three strategies')
        self.assertIsInstance(strategies[0], DMA)
        op = qt.Operator('dma, macd, cdl')
        strategies = op.strategies
        op.info()
        self.assertIsInstance(strategies, list)
        self.assertIsInstance(strategies[0], DMA)
        self.assertIsInstance(strategies[1], MACD)
        self.assertIsInstance(strategies[2], CDL)

    def test_group_properties(self):
        """ test property signal_type"""
        op = qt.Operator()
        op.add_strategies('dma, macd, trix')
        self.assertEqual(op.group_ids, ['Group_1'])
        group = op.groups['Group_1']
        self.assertIsInstance(group.signal_type, str)
        self.assertEqual(group.signal_type, 'pt')
        self.assertEqual(group.run_freq, 'd')
        self.assertEqual(group.run_timing, 'close')
        self.assertEqual(group.strategy_count, 3)
        self.assertEqual(group.members, [op['dma'], op['macd'], op['trix']])
        # 未设置时使用默认 PT 表达式 s0*s1*s2
        self.assertEqual(group.blender_str, 's0*s1*s2')
        self.assertIsNotNone(group.blender)
        self.assertEqual(group.human_blender, 'dma * macd * trix')

        op.groups['Group_1'].blender_str = 's0+s1+s2'
        self.assertEqual(group.blender_str, 's0+s1+s2')
        self.assertEqual(group.human_blender, 'dma + macd + trix')
        self.assertEqual(group.blender, ['+', 's2', '+', 's1', 's0'])

        op.add_strategies('dma, macd')
        self.assertEqual(group.strategy_count, 5)
        self.assertEqual(group.members, [op['dma'], op['macd'], op['trix'], op['dma_1'], op['macd_1']])
        self.assertEqual(group.blender_str, 's0+s1+s2')
        self.assertEqual(group.human_blender, 'dma + macd + trix')
        self.assertEqual(group.blender, ['+', 's2', '+', 's1', 's0'])

        group.blender_str = 's0*2+s3-s1+max(s1, s2)'
        self.assertEqual(group.blender_str, 's0*2+s3-s1+max(s1, s2)')
        self.assertEqual(group.human_blender, 'dma * 2 + dma_1 - macd + max(macd, trix)')
        self.assertEqual(group.blender, ['+', 'max(2)', 's2', 's1', '-', 's1', '+', 's3', '*', '2', 's0'])

        op.add_strategies('dma', run_freq='h', run_timing='open')
        self.assertEqual(op.get_group_by_id('Group_1'), op.groups['Group_1'])
        self.assertEqual(op.get_group_by_id('Group_2'), op.groups['Group_2'])
        self.assertRaises(KeyError, op.get_group_by_id, 'not_exist')

    def test_property_op_data_types(self):
        """ test property op_data_types"""
        op = qt.Operator()
        self.assertIsInstance(op.op_data_types, list)
        self.assertEqual(op.op_data_types, [])

        op = qt.Operator('macd, dma, trix')
        dt_ids = op.op_data_type_ids
        dt = op.op_data_types
        self.assertEqual(dt_ids[0], 'close_ANY_d')
        self.assertEqual(dt[0], DataType('close', freq='d', asset_type='ANY'))

        op = qt.Operator('macd, cdl')
        dt_ids = op.op_data_type_ids
        dt = op.op_data_types
        self.assertIn('close_ANY_d', dt_ids)
        self.assertIn('high_ANY_d', dt_ids)
        self.assertIn('low_ANY_d', dt_ids)
        self.assertIn('open_ANY_d', dt_ids)
        self.assertIn(DataType('close', freq='d', asset_type='ANY'), dt)
        self.assertIn(DataType('high', freq='d', asset_type='ANY'), dt)
        self.assertIn(DataType('low', freq='d', asset_type='ANY'), dt)
        self.assertIn(DataType('open', freq='d', asset_type='ANY'), dt)
        op.add_strategy('dma')
        dt_ids = op.op_data_type_ids
        dt = op.op_data_types
        self.assertIn('close_ANY_d', dt_ids)
        self.assertIn('high_ANY_d', dt_ids)
        self.assertIn('low_ANY_d', dt_ids)
        self.assertIn('open_ANY_d', dt_ids)
        self.assertIn(DataType('close', freq='d', asset_type='ANY'), dt)
        self.assertIn(DataType('high', freq='d', asset_type='ANY'), dt)
        self.assertIn(DataType('low', freq='d', asset_type='ANY'), dt)
        self.assertIn(DataType('open', freq='d', asset_type='ANY'), dt)

    def test_property_op_data_type_count(self):
        """ test property op_data_type_count"""
        op = qt.Operator()
        self.assertIsInstance(op.op_data_type_count, int)
        self.assertEqual(op.op_data_type_count, 0)

        op = qt.Operator('macd, dma, trix')
        dtn = op.op_data_type_count
        self.assertEqual(dtn, 1)
        op = qt.Operator('macd, cdl')
        dtn = op.op_data_type_count
        self.assertEqual(dtn, 4)
        op.add_strategy('dma')
        dtn = op.op_data_type_count
        self.assertEqual(dtn, 4)

    def test_property_opt_space_par(self):
        """ test property opt_space_par"""
        print(f'-----test property opt_space_par--------:\n')
        op = qt.Operator()
        self.assertIsInstance(op.opt_space_par, tuple)
        self.assertIsInstance(op.opt_space_par[0], list)
        self.assertIsInstance(op.opt_space_par[1], list)
        self.assertEqual(len(op.opt_space_par), 2)
        self.assertEqual(op.opt_space_par, ([], []))

        op = qt.Operator('macd, dma, trix, cdl')
        osp = op.opt_space_par
        print(f'before setting opt_tags opt_space_par is empty:\n'
              f'osp is {osp}\n')
        self.assertIsInstance(osp, tuple)
        self.assertEqual(osp[0], [])
        self.assertEqual(osp[1], [])
        op.set_parameter('macd', opt_tag=1)
        op.set_parameter('dma', opt_tag=1)
        osp = op.opt_space_par
        print(f'after setting opt_tags opt_space_par is not empty:\n'
              f'osp is {osp}\n')
        self.assertIsInstance(osp, tuple)
        self.assertEqual(len(osp), 2)
        self.assertIsInstance(osp[0], list)
        self.assertIsInstance(osp[1], list)
        self.assertEqual(len(osp[0]), 6)
        self.assertEqual(len(osp[1]), 6)
        self.assertEqual(osp[0], [(10, 250), (10, 250), (5, 250), (10, 250), (10, 250), (5, 250)])
        self.assertEqual(osp[1], ['int', 'int', 'int', 'int', 'int', 'int'])

    def test_property_opt_types(self):
        """ test property opt_tags"""
        print(f'-----test property opt_tags--------:\n')
        op = qt.Operator()
        self.assertIsInstance(op.opt_tags, list)
        self.assertEqual(len(op.opt_tags), 0)
        self.assertEqual(op.opt_tags, [])

        op = qt.Operator('macd, dma, trix, cdl')
        otp = op.opt_tags
        print(f'before setting opt_tags opt_space_par is empty:\n'
              f'otp is {otp}\n')
        self.assertIsInstance(otp, list)
        self.assertEqual(otp, [0, 0, 0, 0])
        op.set_parameter('macd', opt_tag=1)
        op.set_parameter('dma', opt_tag=1)
        otp = op.opt_tags
        print(f'after setting opt_tags opt_space_par is not empty:\n'
              f'otp is {otp}\n')
        self.assertIsInstance(otp, list)
        self.assertEqual(len(otp), 4)
        self.assertEqual(otp, [1, 1, 0, 0])

    def test_property_max_window_length(self):
        """ test property max_window_length"""
        print(f'-----test property max window length--------:\n')
        op = qt.Operator()
        self.assertIsInstance(op.max_window_length, int)
        self.assertEqual(op.max_window_length, 0)

        op = qt.Operator('macd, dma, trix, ndayvol')
        mwl = op.max_window_length
        print(f'before setting window_length the value is 270:\n'
              f'mwl is {mwl}\n')
        print(f'before setting window_length the max_window_length by dtype "close_ANY_d" is '
              f'{op.get_max_window_length_by_dtype_id("close_ANY_d")}:')
        print(f'before setting window_length the max_window_length by dtype "high_ANY_d" is '
              f'{op.get_max_window_length_by_dtype_id("high_ANY_d")}:')
        self.assertIsInstance(mwl, int)
        self.assertEqual(mwl, 270)
        self.assertEqual(op.get_max_window_length_by_dtype_id('close_ANY_d'), 270)
        self.assertEqual(op.get_max_window_length_by_dtype_id('high_ANY_d'), 150)
        self.assertEqual(op.get_max_window_length_by_dtype_id('low_ANY_d'), 150)

        op.set_parameter('macd', window_length=300)
        op.set_parameter('ndayvol', window_length=350)
        mwl = op.max_window_length
        print(f'after setting window_length the value is new set value:\n'
              f'mwl is {mwl}\n')
        print(f'after setting window_length the max_window_length by dtype "close_E_d" is '
              f'{op.get_max_window_length_by_dtype_id("close_ANY_d")}:')
        print(f'after setting window_length the max_window_length by dtype "high_E_d" is '
              f'{op.get_max_window_length_by_dtype_id("high_ANY_d")}:')
        self.assertIsInstance(mwl, int)
        self.assertEqual(mwl, 350)
        self.assertEqual(op.get_max_window_length_by_dtype_id('close_ANY_d'), 350)
        self.assertEqual(op.get_max_window_length_by_dtype_id('high_ANY_d'), 350)

        # update window_length with multiple values
        op.set_parameter('trix', window_length=(120,))
        op.set_parameter('macd', window_length=(90,))
        op.set_parameter('dma', window_length=(30,))
        op.set_parameter('ndayvol', window_length=(90, 135, 45))

        print(f'set window_length again:')
        self.assertEqual(op['trix'].window_lengths, {'close_ANY_d': 120})
        self.assertEqual(op['macd'].window_lengths, {'close_ANY_d': 90})
        self.assertEqual(op['dma'].window_lengths, {'close_ANY_d': 30})
        self.assertEqual(op['ndayvol'].window_lengths, {'close_ANY_d': 45, 'high_ANY_d': 90, 'low_ANY_d': 135})
        self.assertEqual(op.max_window_length, 135)
        print(f'after setting window_length the max_window_length by dtype "close_ANY_d" is '
              f'{op.get_max_window_length_by_dtype_id("close_ANY_d")}:')
        print(f'after setting window_length the max_window_length by dtype "high_ANY_d" is '
              f'{op.get_max_window_length_by_dtype_id("high_ANY_d")}:')
        print(f'after setting window_length the max_window_length by dtype "low_ANY_d" is '
              f'{op.get_max_window_length_by_dtype_id("low_ANY_d")}:')

        self.assertEqual(op.get_max_window_length_by_dtype_id('close_ANY_d'), 120)
        self.assertEqual(op.get_max_window_length_by_dtype_id('high_ANY_d'), 90)
        self.assertEqual(op.get_max_window_length_by_dtype_id('low_ANY_d'), 135)

    def test_group_property_set(self):
        """ test all property setters:
            setting following properties:
            - strategy_blenders
            - signal_type
            other properties can not be set"""
        print(f'------- Test setting parameters and group properties ---------')
        op_min = qt.Operator(strategies='DMA, MACD, ALL', run_freq='d', run_timing='open', signal_type='PS')
        op_min.set_blender(blender='(s0+s1)*s2')

        self.assertEqual(op_min.groups['Group_1'].run_freq, 'd')
        self.assertEqual(op_min.groups['Group_1'].blender_str, '(s0+s1)*s2')
        self.assertEqual(op_min.groups['Group_1'].human_blender, '(dma + macd) * all')
        self.assertEqual(op_min.groups['Group_1'].blender, ['*', 's2', '+', 's1', 's0'])

        op = qt.Operator('macd, dma, trix, cdl')
        # ckeck that op has only one group with all strategies
        self.assertEqual(op.strategy_group_count, 1)
        self.assertEqual(op.group_ids, ['Group_1'])
        self.assertEqual(op.strategy_groups, {'Group_1': op._groups[0]
                                              })
        self.assertEqual(op.get_strategy_id_by_group('Group_1'), ['macd', 'dma', 'trix', 'cdl'])
        self.assertEqual(op.groups['Group_1'].run_freq, 'd')
        self.assertEqual(op.groups['Group_1'].run_timing, 'close')
        self.assertEqual(op.groups['Group_1'].signal_type, 'pt')
        self.assertEqual(op.groups['Group_1'].blender_str, 's0*s1*s2*s3')
        self.assertIsNotNone(op.groups['Group_1'].blender)

        # change run_freq and run_timing of some of the strategies, then groups will be changed
        op.set_parameter('dma', run_timing='open')
        op.set_parameter('cdl', run_freq='w')
        # check that op has two groups now
        self.assertEqual(op.strategy_group_count, 3)
        self.assertEqual(op.group_ids, ['Group_1', 'Group_2', 'Group_3'])
        self.assertEqual(op.strategy_groups, {'Group_1': op._groups[0],
                                              'Group_2': op._groups[1],
                                              'Group_3': op._groups[2]}
                         )
        self.assertEqual(op.get_strategy_id_by_group('Group_1'), ['macd', 'trix'])
        self.assertEqual(op.get_strategy_id_by_group('Group_2'), ['dma'])
        self.assertEqual(op.get_strategy_id_by_group('Group_3'), ['cdl'])

        self.assertEqual(op.groups['Group_1'].run_freq, 'd')
        self.assertEqual(op.groups['Group_1'].run_timing, 'close')
        self.assertEqual(op.groups['Group_1'].signal_type, 'pt')
        self.assertEqual(op.groups['Group_1'].blender_str, 's0*s1')
        self.assertIsNotNone(op.groups['Group_1'].blender)
        self.assertEqual(op.groups['Group_2'].run_freq, 'd')
        self.assertEqual(op.groups['Group_2'].run_timing, 'open')
        self.assertEqual(op.groups['Group_2'].signal_type, 'pt')
        self.assertEqual(op.groups['Group_2'].blender_str, 's0')
        self.assertIsNotNone(op.groups['Group_2'].blender)
        self.assertEqual(op.groups['Group_3'].run_freq, 'w')
        self.assertEqual(op.groups['Group_3'].run_timing, 'close')
        self.assertEqual(op.groups['Group_3'].signal_type, 'pt')
        self.assertEqual(op.groups['Group_3'].blender_str, 's0')
        self.assertIsNotNone(op.groups['Group_3'].blender)

        # check all strategies are still in op.strategies
        self.assertEqual(op.strategy_count, 4)
        self.assertEqual(op.strategy_ids, ['macd', 'trix', 'dma', 'cdl'])

        # test setting group properties: run_timing, run_freq, signal_type, blender_str
        op.set_group_parameters('Group_1', signal_type='VS')
        self.assertEqual(op.groups['Group_1'].signal_type, 'vs')
        op.set_group_parameters('Group_1', blender_str='s0+s1')
        self.assertEqual(op.groups['Group_1'].blender_str, 's0+s1')
        self.assertEqual(op.groups['Group_1'].human_blender, 'macd + trix')
        self.assertEqual(op.groups['Group_1'].blender, ['+', 's1', 's0'])

        op.set_group_parameters('Group_2', run_timing='close', run_freq='1min')
        # check that Group_2 properties are changed
        self.assertEqual(op.groups['Group_2'].run_timing, 'close')
        self.assertEqual(op.groups['Group_2'].run_freq, '1min')
        self.assertEqual(op.groups['Group_2'].signal_type, 'pt')
        self.assertEqual(op.groups['Group_2'].blender_str, 's0')
        # check that all strategies in group 2 has new run_timing and run_freq
        self.assertEqual(op['dma'].run_timing, 'close')
        self.assertEqual(op['dma'].run_freq, '1min')

        op.set_group_parameters('Group_3', run_timing='open', run_freq='30min', signal_type='PS', blender_str='s0')
        # check that Group_3 properties are changed
        self.assertEqual(op.groups['Group_3'].run_timing, 'open')
        self.assertEqual(op.groups['Group_3'].run_freq, '30min')
        self.assertEqual(op.groups['Group_3'].signal_type, 'ps')
        self.assertEqual(op.groups['Group_3'].blender_str, 's0')
        # check that all strategies in group 3 has new run_timing and run_freq
        self.assertEqual(op['cdl'].run_timing, 'open')
        self.assertEqual(op['cdl'].run_freq, '30min')

        # check that setting group3 the same run freq and run timing as group1 will merge the two groups
        op.set_group_parameters('Group_3', run_timing='close', run_freq='d')
        self.assertEqual(op.strategy_group_count, 2)
        self.assertEqual(op.group_ids, ['Group_1', 'Group_2'])
        self.assertEqual(op.get_strategy_id_by_group('Group_1'), ['macd', 'trix', 'cdl'])
        self.assertEqual(op.get_strategy_id_by_group('Group_2'), ['dma'])
        self.assertEqual(op.groups['Group_1'].run_freq, 'd')
        self.assertEqual(op.groups['Group_1'].run_timing, 'close')
        self.assertEqual(op.groups['Group_1'].signal_type, 'vs')  #
        self.assertEqual(op.groups['Group_1'].blender_str, 's0+s1')  # blender info of group1 is lost after merging
        self.assertEqual(op.groups['Group_1'].human_blender, 'macd + trix')
        self.assertEqual(op.groups['Group_2'].run_freq, '1min')
        self.assertEqual(op.groups['Group_2'].run_timing, 'close')
        self.assertEqual(op.groups['Group_2'].signal_type, 'pt')
        self.assertEqual(op.groups['Group_2'].blender_str, 's0')
        self.assertEqual(op.groups['Group_2'].human_blender, 'dma')
        # check that group_3 no longer exists
        self.assertEqual(op.group_ids, ['Group_1', 'Group_2'])
        self.assertRaises(KeyError, op.get_group_by_id, 'Group_3')
        # check that all strategies are still in op.strategies
        self.assertEqual(op.strategy_count, 4)
        self.assertEqual(op.strategy_ids, ['macd', 'trix', 'cdl', 'dma'])

        # test Error raising for setting parameters: no strategy,
        self.assertRaises(AssertionError, op.set_parameter, None, par_values=(20, 15))
        self.assertRaises(TypeError, op.set_parameter, 'macd', pars='wrong_pars')
        self.assertRaises(TypeError, op.set_parameter, 'macd', par_range='wrong_type')
        self.assertRaises(ValueError, op.set_parameter, 'macd', par_range=((32, 34)))  # wrong length

    def test_operator_ready(self):
        """test the method ready of Operator"""
        op = qt.Operator()
        print(f'operator is ready? "{op.ready}"')
        self.assertEqual(op.ready, False)
        print(f'checking why operator is not ready:\n')
        op.is_ready(
                tell_me_why=True,
        )

        print(f'Adding strategies to operator')
        op.add_strategies('dma, macd')
        print(f'operator is ready? "{op.ready}"')
        self.assertEqual(op.ready, False)
        print(f'checking why operator is not ready:\n')
        op.is_ready(
                tell_me_why=True,
        )

        print(f'Adding one more group of strategies and Setting up blenders for groups')
        op.add_strategies('trix, cdl', run_freq='h', run_timing='open')
        op.set_group_parameters('Group_1', blender_str='s0+s1')
        print(f'operator is ready? "{op.ready}"')
        self.assertEqual(op.ready, False)
        print(f'checking why operator is not ready:\n')
        op.is_ready(
                tell_me_why=True,
        )
        op.set_group_parameters('Group_2', blender_str='s0*s1')
        print(f'operator is ready? "{op.ready}"')
        self.assertEqual(op.ready, False)
        print(f'checking why operator is not ready:\n')
        op.is_ready(
                tell_me_why=True,
        )

        print(f'Adding historical data buffer to operator')
        # set strategy window lengths to be about 5 to 9
        op.set_parameter('dma', window_length=3)
        op.set_parameter('macd', window_length=6)
        op.set_parameter('trix', window_length=9)
        op.set_parameter('cdl', window_length=5)
        self.assertEqual(op.max_window_length, 9)
        all_dtype_ids = op.op_data_type_ids
        data_buffer = {}
        for dtype in all_dtype_ids:
            data_buffer[dtype] = close_d_df
        start = close_d_df.index[10]
        end = close_d_df.index[-1]
        op.prepare_data_buffer(
                start_date=start,
                end_date=end,
                data_package=data_buffer,
        )
        print(f'operator is ready? "{op.ready}"')
        self.assertEqual(op.ready, False)
        print(f'checking why operator is not ready:\n')
        op.is_ready(
                tell_me_why=True,
        )

        print(f'Preparing running schedule for operator')
        op.prepare_running_schedule(
                start_date=start,
                end_date=end,
        )
        print(f'operator is ready? "{op.ready}"')
        self.assertEqual(op.ready, False)
        print(f'checking why operator is not ready:\n')
        op.is_ready(
                tell_me_why=True,
        )

        print(f'Setting up data windows for strategies')
        op.create_data_windows()
        print(f'operator is ready? "{op.ready}"')
        self.assertEqual(op.ready, False)
        print(f'checking why operator is not ready:\n')
        op.is_ready(
                tell_me_why=True,
        )

        print(f'Setting up shares for operator')
        op.set_shares(['share_1', 'share_2'])
        print(f'operator is ready? "{op.ready}"')
        self.assertEqual(op.ready, True)
        print(f'checking why operator is not ready:\n')
        op.is_ready(
                tell_me_why=True,
        )

    def test_operator_prepare_schedule(self):
        """测试Operator生成运行计划"""
        # test function prepare_running_schedule() with group_merge_type = None

        op = qt.Operator()  # create an operator with many strategy groups with freq and timing diversity
        op.add_strategies('dma, macd')  # first group: d@close
        op.add_strategies('dma, macd', run_freq='h')  # second group: h@close
        op.add_strategies('dma, macd', run_freq='d', run_timing='open')  # third group: d@open
        op.add_strategies('dma, macd', run_freq='30min', run_timing='open')  # fourth group: 30min@open
        op.add_strategies('dma, macd', run_freq='d', run_timing='10:00')  # fifth group: d@10:00
        # op.add_strategies('dma, macd', run_freq='W-TUE', run_timing='11:00')  # sixth group: d@9:30

        # set group property of signal type to mix pt/ps/vs types
        op.set_group_parameters(group='Group_1', signal_type='ps')
        op.set_group_parameters(group='Group_3', signal_type='vs')
        op.set_group_parameters(group='Group_4', signal_type='vs')

        op.prepare_running_schedule(
                start_date='2020-01-01',
                end_date='2020-01-04',
                trade_days_only=True,
        )
        op.group_merge_type = 'None'
        print(f'operator running schedule is:\n')
        for group in op.groups:
            print(f'Group schedule for: "{group}:{op.groups[group]}"')
            print(op.group_schedules[group])

        # check details of the schedule of group 1
        schedule = op.group_schedules['Group_1']
        self.assertIsInstance(schedule, pd.DataFrame)
        self.assertEqual(len(schedule.index), 2)
        self.assertEqual(schedule.index[0], pd.Timestamp('2020-01-02 15:00:00'))
        self.assertEqual(schedule.index[-1], pd.Timestamp('2020-01-03 15:00:00'))

        # check details of the schedule of group 2
        schedule = op.group_schedules['Group_2']
        self.assertIsInstance(schedule, pd.DataFrame)
        self.assertEqual(len(schedule.index), 10)
        self.assertEqual(schedule.index[0], pd.Timestamp('2020-01-02 09:30:00'))
        self.assertEqual(schedule.index[1], pd.Timestamp('2020-01-02 10:30:00'))
        self.assertEqual(schedule.index[2], pd.Timestamp('2020-01-02 11:30:00'))
        self.assertEqual(schedule.index[3], pd.Timestamp('2020-01-02 14:00:00'))
        self.assertEqual(schedule.index[4], pd.Timestamp('2020-01-02 15:00:00'))
        self.assertEqual(schedule.index[9], pd.Timestamp('2020-01-03 15:00:00'))

        # check details of the schedule of group 3
        schedule = op.group_schedules['Group_3']
        self.assertIsInstance(schedule, pd.DataFrame)
        self.assertEqual(len(schedule.index), 2)
        self.assertEqual(schedule.index[0], pd.Timestamp('2020-01-02 09:30:00'))
        self.assertEqual(schedule.index[-1], pd.Timestamp('2020-01-03 09:30:00'))

        # check details of the schedule of group 4
        schedule = op.group_schedules['Group_4']
        self.assertIsInstance(schedule, pd.DataFrame)
        self.assertEqual(len(schedule.index), 18)
        self.assertEqual(schedule.index[0], pd.Timestamp('2020-01-02 09:30:00'))
        self.assertEqual(schedule.index[1], pd.Timestamp('2020-01-02 10:00:00'))
        self.assertEqual(schedule.index[2], pd.Timestamp('2020-01-02 10:30:00'))
        self.assertEqual(schedule.index[3], pd.Timestamp('2020-01-02 11:00:00'))
        self.assertEqual(schedule.index[4], pd.Timestamp('2020-01-02 11:30:00'))
        self.assertEqual(schedule.index[5], pd.Timestamp('2020-01-02 13:30:00'))
        self.assertEqual(schedule.index[6], pd.Timestamp('2020-01-02 14:00:00'))
        self.assertEqual(schedule.index[7], pd.Timestamp('2020-01-02 14:30:00'))
        self.assertEqual(schedule.index[8], pd.Timestamp('2020-01-02 15:00:00'))
        self.assertEqual(schedule.index[9], pd.Timestamp('2020-01-03 09:30:00'))
        self.assertEqual(schedule.index[10], pd.Timestamp('2020-01-03 10:00:00'))
        self.assertEqual(schedule.index[11], pd.Timestamp('2020-01-03 10:30:00'))
        self.assertEqual(schedule.index[12], pd.Timestamp('2020-01-03 11:00:00'))
        self.assertEqual(schedule.index[13], pd.Timestamp('2020-01-03 11:30:00'))
        self.assertEqual(schedule.index[14], pd.Timestamp('2020-01-03 13:30:00'))
        self.assertEqual(schedule.index[15], pd.Timestamp('2020-01-03 14:00:00'))
        self.assertEqual(schedule.index[16], pd.Timestamp('2020-01-03 14:30:00'))
        self.assertEqual(schedule.index[17], pd.Timestamp('2020-01-03 15:00:00'))
        self.assertEqual(schedule.index[-1], pd.Timestamp('2020-01-03 15:00:00'))

        # check details of the schedule of group 5
        schedule = op.group_schedules['Group_5']
        self.assertIsInstance(schedule, pd.DataFrame)
        self.assertEqual(len(schedule.index), 2)
        self.assertEqual(schedule.index[0], pd.Timestamp('2020-01-02 10:00:00'))
        self.assertEqual(schedule.index[-1], pd.Timestamp('2020-01-03 10:00:00'))

        timing_table = op.group_timing_table
        op_signal_type = op.op_signal_types
        op_signal_index = op.op_signal_index
        print(f'timing table is {timing_table}')
        print(f'signal types: {op_signal_type}')
        print(f'signal index: {op_signal_index}')
        self.assertEqual(len(op_signal_index), len(op_signal_type))

        # test function prepare_running_schedule() with group_merge_type = 'OR'

        op = qt.Operator()  # create an operator with many strategy groups with freq and timing diversity
        op.add_strategies('dma, macd')  # first group: d@close
        op.add_strategies('dma, macd', run_freq='h')  # second group: h@close
        op.add_strategies('dma, macd', run_freq='d', run_timing='open')  # third group: d@open
        op.add_strategies('dma, macd', run_freq='30min', run_timing='open')  # fourth group: 30min@open
        op.add_strategies('dma, macd', run_freq='d', run_timing='10:00')  # fifth group: d@10:00
        # op.add_strategies('dma, macd', run_freq='W-TUE', run_timing='11:00')  # sixth group: d@9:30

        op.prepare_running_schedule(
                start_date='2020-01-01',
                end_date='2020-01-04',
                trade_days_only=True,
        )
        op.group_merge_type = 'OR'
        print(f'operator running schedule is:\n')
        for group in op.groups:
            print(f'Group schedule for: "{group}:{op.groups[group]}"')
            print(op.group_schedules[group])

        # check details of the schedule of group 1
        schedule = op.group_schedules['Group_1']
        self.assertIsInstance(schedule, pd.DataFrame)
        self.assertEqual(len(schedule.index), 2)
        self.assertEqual(schedule.index[0], pd.Timestamp('2020-01-02 15:00:00'))
        self.assertEqual(schedule.index[-1], pd.Timestamp('2020-01-03 15:00:00'))

        # check details of the schedule of group 2
        schedule = op.group_schedules['Group_2']
        self.assertIsInstance(schedule, pd.DataFrame)
        self.assertEqual(len(schedule.index), 10)
        self.assertEqual(schedule.index[0], pd.Timestamp('2020-01-02 09:30:00'))
        self.assertEqual(schedule.index[1], pd.Timestamp('2020-01-02 10:30:00'))
        self.assertEqual(schedule.index[2], pd.Timestamp('2020-01-02 11:30:00'))
        self.assertEqual(schedule.index[3], pd.Timestamp('2020-01-02 14:00:00'))
        self.assertEqual(schedule.index[4], pd.Timestamp('2020-01-02 15:00:00'))
        self.assertEqual(schedule.index[9], pd.Timestamp('2020-01-03 15:00:00'))

        # check details of the schedule of group 3
        schedule = op.group_schedules['Group_3']
        self.assertIsInstance(schedule, pd.DataFrame)
        self.assertEqual(len(schedule.index), 2)
        self.assertEqual(schedule.index[0], pd.Timestamp('2020-01-02 09:30:00'))
        self.assertEqual(schedule.index[-1], pd.Timestamp('2020-01-03 09:30:00'))

        # check details of the schedule of group 4
        schedule = op.group_schedules['Group_4']
        self.assertIsInstance(schedule, pd.DataFrame)
        self.assertEqual(len(schedule.index), 18)
        self.assertEqual(schedule.index[0], pd.Timestamp('2020-01-02 09:30:00'))
        self.assertEqual(schedule.index[1], pd.Timestamp('2020-01-02 10:00:00'))
        self.assertEqual(schedule.index[2], pd.Timestamp('2020-01-02 10:30:00'))
        self.assertEqual(schedule.index[3], pd.Timestamp('2020-01-02 11:00:00'))
        self.assertEqual(schedule.index[4], pd.Timestamp('2020-01-02 11:30:00'))
        self.assertEqual(schedule.index[5], pd.Timestamp('2020-01-02 13:30:00'))
        self.assertEqual(schedule.index[6], pd.Timestamp('2020-01-02 14:00:00'))
        self.assertEqual(schedule.index[7], pd.Timestamp('2020-01-02 14:30:00'))
        self.assertEqual(schedule.index[8], pd.Timestamp('2020-01-02 15:00:00'))
        self.assertEqual(schedule.index[9], pd.Timestamp('2020-01-03 09:30:00'))
        self.assertEqual(schedule.index[10], pd.Timestamp('2020-01-03 10:00:00'))
        self.assertEqual(schedule.index[11], pd.Timestamp('2020-01-03 10:30:00'))
        self.assertEqual(schedule.index[12], pd.Timestamp('2020-01-03 11:00:00'))
        self.assertEqual(schedule.index[13], pd.Timestamp('2020-01-03 11:30:00'))
        self.assertEqual(schedule.index[14], pd.Timestamp('2020-01-03 13:30:00'))
        self.assertEqual(schedule.index[15], pd.Timestamp('2020-01-03 14:00:00'))
        self.assertEqual(schedule.index[16], pd.Timestamp('2020-01-03 14:30:00'))
        self.assertEqual(schedule.index[17], pd.Timestamp('2020-01-03 15:00:00'))
        self.assertEqual(schedule.index[-1], pd.Timestamp('2020-01-03 15:00:00'))

        # check details of the schedule of group 5
        schedule = op.group_schedules['Group_5']
        self.assertIsInstance(schedule, pd.DataFrame)
        self.assertEqual(len(schedule.index), 2)
        self.assertEqual(schedule.index[0], pd.Timestamp('2020-01-02 10:00:00'))
        self.assertEqual(schedule.index[-1], pd.Timestamp('2020-01-03 10:00:00'))

        timing_table = op.group_timing_table
        op_signal_type = op.op_signal_types
        op_signal_index = op.op_signal_index
        print(f'timing table is {timing_table}')
        print(f'signal types: {op_signal_type}')
        print(f'signal index: {op_signal_index}')
        self.assertEqual(len(op_signal_index), len(op_signal_type))

    def test_operator_assign_history_data(self):
        """测试分配Operator运行所需历史数据"""
        # create an operator with 2 groups of strategies
        op = qt.Operator()
        op.add_strategies([TestGenStg, TestFactorSorter])  # first group: d@close
        op.add_strategies([TestRuleIter], run_freq='h')  # second group: h@close
        print(f'Operator created with {op.strategy_group_count} groups of strategies:\n')
        op.set_parameter('custom', use_latest_data_cycle=False)
        op.set_parameter('custom_1', use_latest_data_cycle=False)
        op.set_parameter('custom_2', use_latest_data_cycle=False)
        op.info(verbose=False)

        op.prepare_running_schedule(
                start_date='2023-01-10',
                end_date='2023-01-31',
                include_start_am=False,
                include_start_pm=False,
        )
        print(f'Operator running schedule prepared:\n')
        for group in op.groups:
            print(f'Group schedule for: "{group}:{op.groups[group]}"')
            print(op.group_schedules[group])
            self.assertIsInstance(op.group_schedules[group], pd.DataFrame)
        group = 'Group_1'
        self.assertEqual(len(op.group_schedules[group].index), 10)
        self.assertEqual(op.group_schedules[group].index[0], pd.Timestamp('2023-01-10 15:00:00'))
        self.assertEqual(op.group_schedules[group].index[-1], pd.Timestamp('2023-01-30 15:00:00'))
        group = 'Group_2'
        self.assertEqual(len(op.group_schedules[group].index), 40)
        self.assertEqual(op.group_schedules[group].index[0], pd.Timestamp('2023-01-10 10:30:00'))
        self.assertEqual(op.group_schedules[group].index[-1], pd.Timestamp('2023-01-30 15:00:00'))

        # test function prepare_data_buffer()

        print(f'Preparing data buffer for operator')
        print(f'Operator data types needed are: {op.op_data_types}')
        data_buffer = {
            'close_E_15min': close_15min_df,
            'close_E_d':     close_d_df,
            'close_E_h':     close_h_df,
            'close_E_w':     close_w_df,
        }
        op.prepare_data_buffer(
                start_date='2023-01-11',
                end_date='2023-01-31',
                data_package=data_buffer,
        )
        print(f'Operator data buffer prepared:\n')
        for dtype in op.op_data_type_ids:
            print(f'Data type "{dtype}" has data:\n')
            print(op.data_buffers[dtype])
            print('\n')

        print(f'Data Windows are NOT created for all strategies')
        self.assertEqual(op.data_window_views, {})
        self.assertEqual(op.data_window_indices, {})

        # test function create_data_windows()
        op.create_data_windows()
        print(f'Data Windows created for all strategies')
        self.assertEqual(len(op.data_window_views), 3)
        self.assertEqual(len(op.data_window_indices), 3)
        data_window_shapes = {
            'custom':   {
                'close_E_d': (22, 4, 3),
                'close_E_h': (56, 5, 3),
            },
            'custom_1': {
                'close_E_d': (23, 1, 3),
                'close_E_w': (3, 1, 3),
            },
            'custom_2': {
                'close_E_h':     (56, 5, 3),
                'close_E_15min': (157, 5, 3),
            },
        }
        data_window_targets = {
            'custom':   {
                'close_E_d': np.array(
                        [[[0.994, 0.412, 0.876],
                          [1.117, 1.257, 1.447],
                          [2.315, 2.08, 2.799],
                          [3.704, 3.87, 3.62]],

                         [[1.117, 1.257, 1.447],
                          [2.315, 2.08, 2.799],
                          [3.704, 3.87, 3.62],
                          [6.091, 6.127, 6.218]],

                         [[2.315, 2.08, 2.799],
                          [3.704, 3.87, 3.62],
                          [6.091, 6.127, 6.218],
                          [7.177, 7.337, 7.294]]],
                ),
                'close_E_h': np.array(
                        [[[0.296, 0.969, 0.422],
                          [0.153, 0.306, 0.254],
                          [0.793, 0.406, 0.798],
                          [0.257, 0.461, 0.749],
                          [1.201, 1.871, 1.381]],

                         [[0.153, 0.306, 0.254],
                          [0.793, 0.406, 0.798],
                          [0.257, 0.461, 0.749],
                          [1.201, 1.871, 1.381],
                          [1.117, 1.665, 1.956]],

                         [[0.793, 0.406, 0.798],
                          [0.257, 0.461, 0.749],
                          [1.201, 1.871, 1.381],
                          [1.117, 1.665, 1.956],
                          [1.04, 1.577, 1.919]]],
                ),
            },
            'custom_1': {
                'close_E_d': np.array(
                        [[[0.994, 0.412, 0.876],
                          [1.117, 1.257, 1.447],
                          [2.315, 2.08, 2.799]],

                         [[1.117, 1.257, 1.447],
                          [2.315, 2.08, 2.799],
                          [3.704, 3.87, 3.62]],

                         [[2.315, 2.08, 2.799],
                          [3.704, 3.87, 3.62],
                          [6.091, 6.127, 6.218]]],

                ),
                'close_E_w': np.array(
                        [[[0.134, 0.207, 0.095]],

                         [[1.015, 0.591, 0.615]],

                         [[1.255, 0.85, 0.992]]],
                ),
            },
            'custom_2': {
                'close_E_h':     np.array(
                        [[[0.296, 0.969, 0.422],
                          [0.153, 0.306, 0.254],
                          [0.793, 0.406, 0.798],
                          [0.257, 0.461, 0.749],
                          [1.201, 1.871, 1.381]],

                         [[0.153, 0.306, 0.254],
                          [0.793, 0.406, 0.798],
                          [0.257, 0.461, 0.749],
                          [1.201, 1.871, 1.381],
                          [1.117, 1.665, 1.956]],

                         [[0.793, 0.406, 0.798],
                          [0.257, 0.461, 0.749],
                          [1.201, 1.871, 1.381],
                          [1.117, 1.665, 1.956],
                          [1.04, 1.577, 1.919]]],
                ),
                'close_E_15min': np.array(
                        [[[3.496, 3.578, 3.001],
                          [3.026, 3.351, 3.198],
                          [3.678, 3.386, 3.985],
                          [3.357, 3.467, 3.014],
                          [3.319, 3.782, 3.705],
                          [3.356, 3.091, 3.209],
                          [3.122, 3.918, 3.729],
                          [3.094, 3.073, 3.343],
                          [3.611, 3.083, 3.345],
                          [3.978, 3.881, 3.738],
                          [3.206, 3.035, 3.697],
                          [3.567, 3.524, 3.884],
                          [3.966, 3.796, 3.849],
                          [3.225, 3.695, 3.128],
                          [3.540, 3.954, 3.556],
                          [3.501, 3.598, 3.956],
                          [6.311, 6.261, 6.459],
                          [6.707, 6.215, 6.338],
                          [6.019, 6.607, 6.370],
                          [6.966, 6.234, 6.653]],

                         [[3.026, 3.351, 3.198],
                          [3.678, 3.386, 3.985],
                          [3.357, 3.467, 3.014],
                          [3.319, 3.782, 3.705],
                          [3.356, 3.091, 3.209],
                          [3.122, 3.918, 3.729],
                          [3.094, 3.073, 3.343],
                          [3.611, 3.083, 3.345],
                          [3.978, 3.881, 3.738],
                          [3.206, 3.035, 3.697],
                          [3.567, 3.524, 3.884],
                          [3.966, 3.796, 3.849],
                          [3.225, 3.695, 3.128],
                          [3.540, 3.954, 3.556],
                          [3.501, 3.598, 3.956],
                          [6.311, 6.261, 6.459],
                          [6.707, 6.215, 6.338],
                          [6.019, 6.607, 6.370],
                          [6.966, 6.234, 6.653],
                          [6.786, 6.136, 6.881]],

                         [[3.678, 3.386, 3.985],
                          [3.357, 3.467, 3.014],
                          [3.319, 3.782, 3.705],
                          [3.356, 3.091, 3.209],
                          [3.122, 3.918, 3.729],
                          [3.094, 3.073, 3.343],
                          [3.611, 3.083, 3.345],
                          [3.978, 3.881, 3.738],
                          [3.206, 3.035, 3.697],
                          [3.567, 3.524, 3.884],
                          [3.966, 3.796, 3.849],
                          [3.225, 3.695, 3.128],
                          [3.540, 3.954, 3.556],
                          [3.501, 3.598, 3.956],
                          [6.311, 6.261, 6.459],
                          [6.707, 6.215, 6.338],
                          [6.019, 6.607, 6.370],
                          [6.966, 6.234, 6.653],
                          [6.786, 6.136, 6.881],
                          [6.900, 6.981, 6.760]]],
                ),
            },
        }
        target_data_indices = {
            'custom':   {
                'close_E_d': np.array(
                        [1, 1, 1, 1, 2, 2, 2, 2, 3, 3,
                         3, 3, 4, 4, 4, 4, 5, 5, 5, 5,
                         6, 6, 6, 6, 7, 7, 7, 7, 8, 8,
                         8, 8, 9, 9, 9, 9, 10, 10, 10, 10]
                ),
                'close_E_h': np.array(
                        [15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
                         25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
                         35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
                         45, 46, 47, 48, 49, 50, 51, 52, 53, 54]
                ),
            },
            'custom_1': {
                'close_E_d': np.array(
                        [2, 2, 2, 2, 3, 3, 3, 3, 4, 4,
                         4, 4, 5, 5, 5, 5, 6, 6, 6, 6,
                         7, 7, 7, 7, 8, 8, 8, 8, 9, 9,
                         9, 9, 10, 10, 10, 10, 11, 11, 11, 11]
                ),
                'close_E_w': np.array(
                        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                         0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
                         1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
                         1, 1, 1, 1, 1, 1, 2, 2, 2, 2]
                ),
            },
            'custom_2': {
                'close_E_h':     np.array(
                        [15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
                         25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
                         35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
                         45, 46, 47, 48, 49, 50, 51, 52, 53, 54]
                ),
                'close_E_15min': np.array(
                        [15, 19, 23, 27, 31, 35, 39, 43, 47, 51,
                         55, 59, 63, 67, 71, 75, 79, 83, 87, 91,
                         95, 99, 103, 107, 111, 115, 119, 123, 127, 131,
                         135, 139, 143, 147, 151, 155, 156, 156, 156, 156]
                ),
            },
        }
        for stg_id in op.strategy_ids:
            print(f'Strategy "{stg_id}" has data window for its data types:\n'
                  f'{op[stg_id].data_types}\n')
            for dtype in op[stg_id].data_types:
                data_window = op.data_window_views[stg_id][dtype]
                data_indices = op.data_window_indices[stg_id][dtype]
                print(f'Data type "{dtype}" has data window (shape: {data_window.shape}):\n'
                      f'{data_window[:3]}\n'
                      f'with window indices: {data_indices}\n'
                      )
                # check data type and data shapes
                self.assertIn(dtype, op.op_data_type_ids)
                self.assertIsInstance(data_window, np.ndarray)
                self.assertIsInstance(data_window, np.ndarray)
                self.assertEqual(len(data_window), data_window_shapes[stg_id][dtype][0])
                self.assertEqual(data_window.shape[1],
                                 op[stg_id].window_lengths[dtype])

                self.assertEqual(data_window.shape[2],
                                 3)
                # check that the data indices are correct
                self.assertTrue(np.allclose(data_window[:3],
                                            data_window_targets[stg_id][dtype], ))
                # check all data indices
                self.assertIsInstance(data_indices, np.ndarray)
                self.assertEqual(len(data_indices), 40)
                self.assertTrue(np.allclose(
                        data_indices,
                        target_data_indices[stg_id][dtype],
                ))

        # set different ULC for strategy data types, and check again data window and data indices
        op.set_parameter('custom',
                         use_latest_data_cycle=[True, False], )
        op.set_parameter('custom_1',
                         use_latest_data_cycle=[False, True], )
        op.set_parameter('custom_2',
                         use_latest_data_cycle=[False, True], )

        op.create_data_windows()

        target_data_indices = {
            'custom':   {
                'close_E_d': np.array(
                        [1, 1, 1, 2, 2, 2, 2, 3, 3, 3,
                         3, 4, 4, 4, 4, 5, 5, 5, 5, 6,
                         6, 6, 6, 7, 7, 7, 7, 8, 8, 8,
                         8, 9, 9, 9, 9, 10, 10, 10, 10, 11]  # USE latest data cycle = TRUE
                ),
                'close_E_h': np.array(
                        [15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
                         25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
                         35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
                         45, 46, 47, 48, 49, 50, 51, 52, 53, 54]
                ),
            },
            'custom_1': {
                'close_E_d': np.array(
                        [2, 2, 2, 2, 3, 3, 3, 3, 4, 4,
                         4, 4, 5, 5, 5, 5, 6, 6, 6, 6,
                         7, 7, 7, 7, 8, 8, 8, 8, 9, 9,
                         9, 9, 10, 10, 10, 10, 11, 11, 11, 11]
                ),
                'close_E_w': np.array(
                        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                         0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
                         1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
                         1, 1, 1, 1, 1, 2, 2, 2, 2, 2]  # USE latest data cycle = TRUE
                ),
            },
            'custom_2': {
                'close_E_h':     np.array(
                        [15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
                         25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
                         35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
                         45, 46, 47, 48, 49, 50, 51, 52, 53, 54]
                ),
                'close_E_15min': np.array(
                        [16, 20, 24, 28, 32, 36, 40, 44, 48, 52,
                         56, 60, 64, 68, 72, 76, 80, 84, 88, 92,
                         96, 100, 104, 108, 112, 116, 120, 124, 128, 132,
                         136, 140, 144, 148, 152, 156, 156, 156, 156, 156]  # USE latest data cycle = TRUE
                ),
            },
        }
        for stg_id in op.strategy_ids:
            print(f'======================AFTER setting different ULC========================'
                  f'Strategy "{stg_id}" has data window for its data types:\n'
                  f'{op[stg_id].data_types}\n')
            for dtype in op[stg_id].data_types:
                data_window = op.data_window_views[stg_id][dtype]
                data_indices = op.data_window_indices[stg_id][dtype]
                print(f'Data type "{dtype}" has data window (shape: {data_window.shape}):\n'
                      f'{data_window[:3]}\n'
                      f'with window indices: {data_indices}\n'
                      )
                # check data type and data shapes
                self.assertIn(dtype, op.op_data_type_ids)
                self.assertIsInstance(data_window, np.ndarray)
                self.assertIsInstance(data_window, np.ndarray)
                self.assertEqual(len(data_window), data_window_shapes[stg_id][dtype][0])
                self.assertEqual(data_window.shape[1],
                                 op[stg_id].window_lengths[dtype])

                self.assertEqual(data_window.shape[2],
                                 3)
                # check that the data indices are correct
                self.assertTrue(np.allclose(data_window[:3],
                                            data_window_targets[stg_id][dtype], ))
                # check all data indices
                self.assertIsInstance(data_indices, np.ndarray)
                self.assertEqual(len(data_indices), 40)
                self.assertTrue(np.allclose(
                        data_indices,
                        target_data_indices[stg_id][dtype],
                ))

    def test_set_opt_par(self):
        """ test setting opt pars in batch"""
        print(f'--------- Testing setting Opt Pars: set_opt_par_values -------')
        op = qt.Operator('dma, random, crossline')
        op.set_parameter('dma',
                         opt_tag=1,
                         window_length=10, )
        op.set_parameter('dma',
                         par_values=(50, 10, 55))
        self.assertEqual(op.strategies[0].par_values, (50, 10, 55))
        self.assertEqual(op.strategies[1].par_values, (0.5,))
        self.assertEqual(op.strategies[2].par_values, (35, 120, 0.02))
        self.assertEqual(op.opt_tags, [1, 0, 0])
        op.set_opt_par_values((58, 12, 90))
        self.assertEqual(op.strategies[0].par_values, (58, 12, 90))
        self.assertEqual(op.strategies[1].par_values, (0.5,))
        self.assertEqual(op.strategies[2].par_values, (35, 120, 0.02))

        op.set_parameter('crossline',
                         opt_tag=1,
                         window_length=10, )
        op.set_parameter('crossline',
                         par_values=(55, 10, 0.1))
        self.assertEqual(op.opt_tags, [1, 0, 1])
        op.set_opt_par_values((60, 12, 99, 80, 26, 0.09))
        self.assertEqual(op.strategies[0].par_values, (60, 12, 99))
        self.assertEqual(op.strategies[1].par_values, (0.5,))
        self.assertEqual(op.strategies[2].par_values, (80, 26, 0.09))

        op.set_opt_par_values((90, 200, 155, 80, 26, 0.09, 5, 12, 9))
        self.assertEqual(op.strategies[0].par_values, (90, 200, 155))
        self.assertEqual(op.strategies[1].par_values, (0.5,))
        self.assertEqual(op.strategies[2].par_values, (80, 26, 0.09))

        # test set_opt_par_values when opt_tag is set to be 2 (enumerate type of parameters)
        op.set_parameter('crossline',
                         opt_tag=2,
                         window_length=10, )
        op.set_parameter('crossline',
                         par_values=(50, 10, 0.05))
        self.assertEqual(op.opt_tags, [1, 0, 2])
        self.assertEqual(op.strategies[0].par_values, (90, 200, 155))
        self.assertEqual(op.strategies[1].par_values, (0.5,))
        self.assertEqual(op.strategies[2].par_values, (50, 10, 0.05))
        op.set_opt_par_values((15, 12, 9, (18, 26, 0.09)))
        self.assertEqual(op.strategies[0].par_values, (15, 12, 9))
        self.assertEqual(op.strategies[1].par_values, (0.5,))
        self.assertEqual(op.strategies[2].par_values, (18, 26, 0.09))

        # Test Errors
        # op.set_opt_par主要在优化过程中自动生成，已经保证了参数的正确性，因此不再检查参数正确性

    def test_operator_signal_creation(self):
        """ 测试operator对象生成完整信号,仅考虑交易信号的生成，不考虑信号的执行以及持仓变化

        生成一个包含两个group，三个交易策略的operator对象，使用测试数据生成交易信号清单
        三个交易策略的运行频率固定，使用的交易数据固定，数据窗口长度固定，运行时间区间也固定
        测试operator的策略在以下不同设置下生成交易信号的正确性

        1，测试交易策略数据类型的ULC参数，首先所有的ULC都为False，然后设置为True与False的组合
        2，测试交易策略不同的parameter参数下交易信号的不同
        3，测试交易策略3的Multi_parameters参数，即不同的股票使用不同的参数
        4，测试不同的group_merge_type：首先使用None，然后测试group_merge_type=‘or'
        """
        # create an operator with 2 groups of strategies, set all ULCs to False
        op = qt.Operator()
        op.add_strategies([TestGenStg, TestFactorSorter])  # first group: d@close
        op.add_strategies([TestRuleIter], run_freq='h')  # second group: h@close
        print(f'Operator created with {op.strategy_group_count} groups of strategies:\n')
        op.set_parameter('custom', use_latest_data_cycle=False, par_values=(4, 0.5))
        op.set_parameter('custom_1', use_latest_data_cycle=False, par_values=(3, 0.5), max_sel_count=2,
                         weighting='even')
        op.set_parameter('custom_2', use_latest_data_cycle=False, par_values=('option1', np.array([1, 2, 3])))
        op.info(verbose=True)

        # set up blender
        op.set_group_parameters(group='Group_1', blender_str='(s0+s1)/2')
        op.set_group_parameters(group='Group_2', blender_str='s0')
        op.group_merge_type = 'None'

        # create running schedule and prepare data buffer and data windows
        op.prepare_running_schedule(
                start_date='2023-01-10',
                end_date='2023-01-31',
                include_start_am=False,
                include_start_pm=False,
        )
        print(f'Operator running schedule prepared:\n')
        # prepare data buffer

        data_buffer = {
            'close_E_15min': close_15min_df,
            'close_E_d':     close_d_df,
            'close_E_h':     close_h_df,
            'close_E_w':     close_w_df,
        }
        op.prepare_data_buffer(
                start_date='2023-01-11',
                end_date='2023-01-31',
                data_package=data_buffer,
        )
        print(f'Operator data buffer prepared:\n')

        # create_data_windows()
        op.create_data_windows()
        print(f'Data Windows created for all strategies\n'
              f'Operator is ready: {op.is_ready(tell_me_why=True)}\n')

        for stg_id in op.strategy_ids:
            print(f'Strategy "{stg_id}" has data window for its data types:\n'
                  f'{op[stg_id].data_types}\n')
            for dtype in op[stg_id].data_types:
                data_window = op.data_window_views[stg_id][dtype]
                data_indices = op.data_window_indices[stg_id][dtype]
                print(f'Data type "{dtype}" has data window (shape: {data_window.shape}):\n'
                      f'{data_window[:3]}\n'
                      f'with window indices: {data_indices}\n'
                      )

        # generate trading signals
        print(f'Generating trading signals with all ULCs set to False\n'
              f'Operator run schedule: length: ({len(op.group_timing_table)})\n'
              f'{op.group_timing_table}\n'
              f'signal_count\n'
              f'{op.get_signal_count()}\n')
        # check that the signals are correct
        signals_target = [
            ('pt', 0, np.array([0., 0., 0.])),
            ('pt', 1, np.array([0., 0.333, 0.333])),
            ('pt', 2, np.array([0., 0.333, 0.333])),
            ('pt', 3, np.array([0.25, 0.25, 0.5])),
            ('pt', 3, np.array([0., 0.333, 0.])),
            ('pt', 4, np.array([0., 0., 0.])),
            ('pt', 5, np.array([0.333, 0., 0.])),
            ('pt', 6, np.array([0., 0.333, 0.333])),
            ('pt', 7, np.array([0.5, 0.25, 0.25])),
            ('pt', 7, np.array([0., 0.333, 0.])),
            ('pt', 8, np.array([0., 0., 0.])),
            ('pt', 9, np.array([0.333, 0., 0.333])),
            ('pt', 10, np.array([0.333, 0.333, 0.333])),
            ('pt', 11, np.array([0.25, 0.25, 0.5])),
            ('pt', 11, np.array([0.333, 0.333, 0.333])),
            ('pt', 12, np.array([0., 0., 0.])),
            ('pt', 13, np.array([0., 0., 0.333])),
            ('pt', 14, np.array([0.333, 0.333, 0.333])),
            ('pt', 15, np.array([0.5, 0., 0.5])),
            ('pt', 15, np.array([0.333, 0., 0.333])),
            ('pt', 16, np.array([0., 0., 0.])),
            ('pt', 17, np.array([0., 0.333, 0.333])),
            ('pt', 18, np.array([0.333, 0.333, 0.333])),
            ('pt', 19, np.array([0.25, 0.5, 0.25])),
            ('pt', 19, np.array([0.333, 0., 0.])),
            ('pt', 20, np.array([0., 0., 0.])),
            ('pt', 21, np.array([0.333, 0.333, 0.333])),
            ('pt', 22, np.array([0.333, 0.333, 0.])),
            ('pt', 23, np.array([0.25, 0.5, 0.25])),
            ('pt', 23, np.array([0.333, 0.333, 0.333])),
            ('pt', 24, np.array([0., 0., 0.])),
            ('pt', 25, np.array([0., 0., 0.])),
            ('pt', 26, np.array([0., 0., 0.])),
            ('pt', 27, np.array([0., 0.5, 0.5])),
            ('pt', 27, np.array([0., 0.333, 0.333])),
            ('pt', 28, np.array([0., 0., 0.])),
            ('pt', 29, np.array([0.333, 0., 0.])),
            ('pt', 30, np.array([0.333, 0.333, 0.])),
            ('pt', 31, np.array([0.25, 0.5, 0.25])),
            ('pt', 31, np.array([0., 0., 0.])),
            ('pt', 32, np.array([0., 0., 0.])),
            ('pt', 33, np.array([0.333, 0.333, 0.333])),
            ('pt', 34, np.array([0.333, 0., 0.])),
            ('pt', 35, np.array([0.25, 0.25, 0.5])),
            ('pt', 35, np.array([0.333, 0., 0.333])),
            ('pt', 36, np.array([0.333, 0.333, 0.333])),
            ('pt', 37, np.array([0.333, 0.333, 0.333])),
            ('pt', 38, np.array([0.333, 0.333, 0.333])),
            ('pt', 39, np.array([0.5, 0.25, 0.25])),
            ('pt', 39, np.array([0.333, 0.333, 0.333])),
        ]
        signals = []
        signal_index = 0
        op.set_shares(['000001', '000002', '000003'])
        for signal in op.run_strategies(steps=range(len(op.group_timing_table))):
            signals.append(signal)
            expected = signals_target[signal_index]
            print(signal, expected)
            self.assertEqual(expected[0], signal[0])
            self.assertEqual(expected[1], signal[1])
            self.assertTrue(np.allclose(expected[2], signal[2], atol=0.001))
            signal_index += 1

        print(f'Generating trading signals completed\n')
        self.assertEqual(len(signals), op.get_signal_count())
        self.assertEqual(len(signals), len(signals_target))

        # 1, set different ULC combinations and check data windows and signals

        op.set_parameter('custom', use_latest_data_cycle=[True, False])
        op.set_parameter('custom_1', use_latest_data_cycle=[False, True])
        op.set_parameter('custom_2', use_latest_data_cycle=True)

        # create_data_windows()
        op.create_data_windows()
        print(f'Data Windows created for all strategies\n'
              f'Operator is ready: {op.is_ready(tell_me_why=True)}\n')

        # generate trading signals
        print(f'Generating trading signals with all ULCs set to False\n'
              f'Operator run schedule:\n'
              f'{op.group_timing_table}({len(op.group_timing_table)})\n'
              f'signal_count\n'
              f'{op.get_signal_count()}\n')
        # check that the signals are correct
        signals_target = [
            ('pt', 0, np.array([0., 0.333, 0.333])),
            ('pt', 1, np.array([0.333, 0.333, 0.333])),
            ('pt', 2, np.array([0., 0., 0.])),
            ('pt', 3, np.array([0.25, 0.25, 0.5])),
            ('pt', 3, np.array([0.333, 0.333, 0.])),
            ('pt', 4, np.array([0.333, 0., 0.])),
            ('pt', 5, np.array([0.333, 0.333, 0.333])),
            ('pt', 6, np.array([0., 0.333, 0.])),
            ('pt', 7, np.array([0.25, 0.25, 0.5])),
            ('pt', 7, np.array([0.333, 0.333, 0.])),
            ('pt', 8, np.array([0.333, 0., 0.333])),
            ('pt', 9, np.array([0.333, 0.333, 0.333])),
            ('pt', 10, np.array([0., 0.333, 0.])),
            ('pt', 11, np.array([0.25, 0.25, 0.5])),
            ('pt', 11, np.array([0., 0.333, 0.333])),
            ('pt', 12, np.array([0., 0.333, 0.333])),
            ('pt', 13, np.array([0., 0.333, 0.333])),
            ('pt', 14, np.array([0.333, 0., 0.])),
            ('pt', 15, np.array([0.25, 0.25, 0.5])),
            ('pt', 15, np.array([0.333, 0., 0.333])),
            ('pt', 16, np.array([0., 0., 0.333])),
            ('pt', 17, np.array([0.333, 0.333, 0.])),
            ('pt', 18, np.array([0.333, 0., 0.333])),
            ('pt', 19, np.array([0.25, 0.5, 0.25])),
            ('pt', 19, np.array([0., 0., 0.])),
            ('pt', 20, np.array([0.333, 0.333, 0.])),
            ('pt', 21, np.array([0., 0., 0.333])),
            ('pt', 22, np.array([0.333, 0.333, 0.])),
            ('pt', 23, np.array([0.25, 0.5, 0.25])),
            ('pt', 23, np.array([0., 0., 0.333])),
            ('pt', 24, np.array([0., 0., 0.])),
            ('pt', 25, np.array([0., 0.333, 0.])),
            ('pt', 26, np.array([0., 0., 0.333])),
            ('pt', 27, np.array([0., 0.5, 0.5])),
            ('pt', 27, np.array([0., 0., 0.333])),
            ('pt', 28, np.array([0.333, 0., 0.])),
            ('pt', 29, np.array([0.333, 0.333, 0.])),
            ('pt', 30, np.array([0., 0., 0.])),
            ('pt', 31, np.array([0.25, 0.5, 0.25])),
            ('pt', 31, np.array([0., 0.333, 0.333])),
            ('pt', 32, np.array([0.333, 0.333, 0.333])),
            ('pt', 33, np.array([0.333, 0., 0.])),
            ('pt', 34, np.array([0., 0., 0.])),
            ('pt', 35, np.array([0.25, 0.25, 0.5])),
            ('pt', 35, np.array([0.333, 0.333, 0.333])),
            ('pt', 36, np.array([0.333, 0.333, 0.333])),
            ('pt', 37, np.array([0.333, 0.333, 0.333])),
            ('pt', 38, np.array([0.333, 0.333, 0.333])),
            ('pt', 39, np.array([0.5, 0.5, 0.])),
            ('pt', 39, np.array([0.333, 0.333, 0.333])),
        ]
        signals = []
        signal_index = 0
        for signal in op.run_strategies(steps=range(len(op.group_timing_table))):
            signals.append(signal)
            expected = signals_target[signal_index]
            print(signal, expected)
            self.assertEqual(expected[0], signal[0])
            self.assertEqual(expected[1], signal[1])
            self.assertTrue(np.allclose(expected[2], signal[2], atol=0.001))
            signal_index += 1

        print(f'Generating trading signals completed\n')
        self.assertEqual(len(signals), op.get_signal_count())
        self.assertEqual(len(signals), len(signals_target))

        # 2, set different parameters for strategies and check signals
        op.set_parameter('custom', par_values=(3, 0.7))
        op.set_parameter('custom_1', par_values=(2, 0.3))
        op.set_parameter('custom_2', par_values=('option2', np.array([3, 2, 1])))

        # create_data_windows()
        op.create_data_windows()
        print(f'Data Windows created for all strategies\n'
              f'Operator is ready: {op.is_ready(tell_me_why=True)}\n')

        # generate trading signals
        print(f'Generating trading signals with all ULCs set to False\n'
              f'Operator run schedule:\n'
              f'{op.group_timing_table}({len(op.group_timing_table)})\n'
              f'signal_count\n'
              f'{op.get_signal_count()}\n')
        # check that the signals are correct
        signals_target = [
            ('pt', 0, np.array([0.333, 0.333, 0.333])),
            ('pt', 1, np.array([0.333, 0.333, 0.333])),
            ('pt', 2, np.array([0.333, 0., 0.])),
            ('pt', 3, np.array([0.5, 0., 0.5])),
            ('pt', 3, np.array([0., 0., 0.333])),
            ('pt', 4, np.array([0.333, 0.333, 0.333])),
            ('pt', 5, np.array([0.333, 0.333, 0.333])),
            ('pt', 6, np.array([0., 0., 0.333])),
            ('pt', 7, np.array([0.25, 0.25, 0.5])),
            ('pt', 7, np.array([0., 0., 0.333])),
            ('pt', 8, np.array([0.333, 0.333, 0.333])),
            ('pt', 9, np.array([0.333, 0., 0.333])),
            ('pt', 10, np.array([0.333, 0., 0.333])),
            ('pt', 11, np.array([0.25, 0.25, 0.5])),
            ('pt', 11, np.array([0., 0., 0.])),
            ('pt', 12, np.array([0.333, 0.333, 0.333])),
            ('pt', 13, np.array([0.333, 0., 0.333])),
            ('pt', 14, np.array([0., 0., 0.333])),
            ('pt', 15, np.array([0.25, 0.25, 0.5])),
            ('pt', 15, np.array([0.333, 0.333, 0.])),
            ('pt', 16, np.array([0.333, 0.333, 0.333])),
            ('pt', 17, np.array([0.333, 0.333, 0.333])),
            ('pt', 18, np.array([0., 0.333, 0.333])),
            ('pt', 19, np.array([0.5, 0.25, 0.25])),
            ('pt', 19, np.array([0., 0., 0.])),
            ('pt', 20, np.array([0.333, 0.333, 0.333])),
            ('pt', 21, np.array([0.333, 0.333, 0.333])),
            ('pt', 22, np.array([0., 0., 0.333])),
            ('pt', 23, np.array([0.25, 0.5, 0.25])),
            ('pt', 23, np.array([0.333, 0., 0.])),
            ('pt', 24, np.array([0.333, 0.333, 0.333])),
            ('pt', 25, np.array([0.333, 0.333, 0.333])),
            ('pt', 26, np.array([0., 0.333, 0.])),
            ('pt', 27, np.array([0.25, 0.5, 0.25])),
            ('pt', 27, np.array([0.333, 0.333, 0.])),
            ('pt', 28, np.array([0.333, 0.333, 0.333])),
            ('pt', 29, np.array([0.333, 0.333, 0.333])),
            ('pt', 30, np.array([0., 0.333, 0.333])),
            ('pt', 31, np.array([0.5, 0., 0.5])),
            ('pt', 31, np.array([0.333, 0., 0.333])),
            ('pt', 32, np.array([0.333, 0.333, 0.333])),
            ('pt', 33, np.array([0.333, 0.333, 0.333])),
            ('pt', 34, np.array([0., 0.333, 0.333])),
            ('pt', 35, np.array([0.5, 0.5, 0.])),
            ('pt', 35, np.array([0., 0.333, 0.])),
            ('pt', 36, np.array([0., 0., 0.])),
            ('pt', 37, np.array([0., 0., 0.])),
            ('pt', 38, np.array([0., 0., 0.])),
            ('pt', 39, np.array([0.25, 0.5, 0.25])),
            ('pt', 39, np.array([0., 0., 0.])),
        ]
        signals = []
        signal_index = 0
        for signal in op.run_strategies(steps=range(len(op.group_timing_table))):
            signals.append(signal)
            expected = signals_target[signal_index]
            print(signal, expected)
            self.assertEqual(expected[0], signal[0])
            self.assertEqual(expected[1], signal[1])
            self.assertTrue(np.allclose(expected[2], signal[2], atol=0.001))
            signal_index += 1

        print(f'Generating trading signals completed\n')
        self.assertEqual(len(signals), op.get_signal_count())
        self.assertEqual(len(signals), len(signals_target))

        # 3, set different Multi_parameters for strategy 3 and check signals
        op.set_parameter('custom_2', allow_multi_par=True,
                         multi_pars=[
                             ('option1', np.array([1, 2, 3])),
                             ('option2', np.array([3, 2, 1])),
                             ('option3', np.array([2, 3, 1])),
                         ])
        # create_data_windows()
        op.create_data_windows()
        print(f'Data Windows created for all strategies\n'
              f'Operator is ready: {op.is_ready(tell_me_why=True)}\n')

        # generate trading signals
        print(f'Generating trading signals with multi-parameters \n'
              f'Operator run schedule:\n'
              f'{op.group_timing_table}({len(op.group_timing_table)})\n'
              f'signal_count\n'
              f'{op.get_signal_count()}\n')
        # check that the signals are correct
        signals_target = [
            ('pt', 0, np.array([0., 0.333, 0.333])),
            ('pt', 1, np.array([0.333, 0.333, 0.333])),
            ('pt', 2, np.array([0., 0., 0.333])),
            ('pt', 3, np.array([0.5, 0., 0.5])),
            ('pt', 3, np.array([0.333, 0., 0.333])),
            ('pt', 4, np.array([0.333, 0.333, 0.333])),
            ('pt', 5, np.array([0.333, 0.333, 0.333])),
            ('pt', 6, np.array([0., 0., 0.333])),
            ('pt', 7, np.array([0.25, 0.25, 0.5])),
            ('pt', 7, np.array([0.333, 0., 0.333])),
            ('pt', 8, np.array([0.333, 0.333, 0.])),
            ('pt', 9, np.array([0.333, 0., 0.333])),
            ('pt', 10, np.array([0., 0., 0.333])),
            ('pt', 11, np.array([0.25, 0.25, 0.5])),
            ('pt', 11, np.array([0., 0., 0.])),
            ('pt', 12, np.array([0., 0.333, 0.])),
            ('pt', 13, np.array([0., 0., 0.])),
            ('pt', 14, np.array([0.333, 0., 0.333])),
            ('pt', 15, np.array([0.25, 0.25, 0.5])),
            ('pt', 15, np.array([0.333, 0.333, 0.333])),
            ('pt', 16, np.array([0., 0.333, 0.])),
            ('pt', 17, np.array([0.333, 0.333, 0.333])),
            ('pt', 18, np.array([0.333, 0.333, 0.333])),
            ('pt', 19, np.array([0.5, 0.25, 0.25])),
            ('pt', 19, np.array([0., 0., 0.])),
            ('pt', 20, np.array([0.333, 0.333, 0.])),
            ('pt', 21, np.array([0., 0.333, 0.333])),
            ('pt', 22, np.array([0.333, 0., 0.333])),
            ('pt', 23, np.array([0.25, 0.5, 0.25])),
            ('pt', 23, np.array([0., 0., 0.])),
            ('pt', 24, np.array([0., 0.333, 0.])),
            ('pt', 25, np.array([0., 0.333, 0.])),
            ('pt', 26, np.array([0., 0.333, 0.333])),
            ('pt', 27, np.array([0.25, 0.5, 0.25])),
            ('pt', 27, np.array([0., 0.333, 0.])),
            ('pt', 28, np.array([0.333, 0.333, 0.333])),
            ('pt', 29, np.array([0.333, 0.333, 0.])),
            ('pt', 30, np.array([0., 0.333, 0.])),
            ('pt', 31, np.array([0.5, 0., 0.5])),
            ('pt', 31, np.array([0., 0., 0.333])),
            ('pt', 32, np.array([0.333, 0.333, 0.])),
            ('pt', 33, np.array([0.333, 0.333, 0.333])),
            ('pt', 34, np.array([0., 0.333, 0.])),
            ('pt', 35, np.array([0.5, 0.5, 0.])),
            ('pt', 35, np.array([0.333, 0.333, 0.])),
            ('pt', 36, np.array([0.333, 0., 0.])),
            ('pt', 37, np.array([0.333, 0., 0.])),
            ('pt', 38, np.array([0.333, 0., 0.])),
            ('pt', 39, np.array([0.25, 0.5, 0.25])),
            ('pt', 39, np.array([0.333, 0., 0.])),
        ]
        signals = []
        signal_index = 0
        for signal in op.run_strategies(steps=range(len(op.group_timing_table))):
            signals.append(signal)
            expected = signals_target[signal_index]
            print(signal, expected)
            self.assertEqual(expected[0], signal[0])
            self.assertEqual(expected[1], signal[1])
            self.assertTrue(np.allclose(expected[2], signal[2], atol=0.001))
            signal_index += 1

        print(f'Generating trading signals completed\n')
        self.assertEqual(len(signals), 50)
        self.assertEqual(len(signals), op.get_signal_count())
        self.assertEqual(len(signals), len(signals_target))

        # 4, set group_merge_type to 'or' and check signals
        op.group_merge_type = 'or'

        # create_data_windows()
        op.create_data_windows()
        print(f'Data Windows created for all strategies\n'
              f'Operator is ready: {op.is_ready(tell_me_why=True)}\n')
        # generate trading signals
        print(f'Generating trading signals with some ULCs = True with operator group_merge_type = "or"\n'
              f'Operator run schedule:\n'
              f'{op.group_timing_table}({len(op.group_timing_table)})\n'
              f'signal_count\n'
              f'{op.get_signal_count()}\n')
        # check that the signals are correct
        signals_target = [
            ('pt', 0, np.array([0., 0.333, 0.333])),
            ('pt', 1, np.array([0.333, 0.333, 0.333])),
            ('pt', 2, np.array([0., 0., 0.333])),
            ('pt', 3, np.array([0.833, 0., 0.833])),
            ('pt', 4, np.array([0.333, 0.333, 0.333])),
            ('pt', 5, np.array([0.333, 0.333, 0.333])),
            ('pt', 6, np.array([0., 0., 0.333])),
            ('pt', 7, np.array([0.583, 0.25, 0.833])),
            ('pt', 8, np.array([0.333, 0.333, 0.])),
            ('pt', 9, np.array([0.333, 0., 0.333])),
            ('pt', 10, np.array([0., 0., 0.333])),
            ('pt', 11, np.array([0.25, 0.25, 0.5])),
            ('pt', 12, np.array([0., 0.333, 0.])),
            ('pt', 13, np.array([0., 0., 0.])),
            ('pt', 14, np.array([0.333, 0., 0.333])),
            ('pt', 15, np.array([0.583, 0.583, 0.833])),
            ('pt', 16, np.array([0., 0.333, 0.])),
            ('pt', 17, np.array([0.333, 0.333, 0.333])),
            ('pt', 18, np.array([0.333, 0.333, 0.333])),
            ('pt', 19, np.array([0.5, 0.25, 0.25])),
            ('pt', 20, np.array([0.333, 0.333, 0.])),
            ('pt', 21, np.array([0., 0.333, 0.333])),
            ('pt', 22, np.array([0.333, 0., 0.333])),
            ('pt', 23, np.array([0.25, 0.5, 0.25])),
            ('pt', 24, np.array([0., 0.333, 0.])),
            ('pt', 25, np.array([0., 0.333, 0.])),
            ('pt', 26, np.array([0., 0.333, 0.333])),
            ('pt', 27, np.array([0.25, 0.833, 0.25])),
            ('pt', 28, np.array([0.333, 0.333, 0.333])),
            ('pt', 29, np.array([0.333, 0.333, 0.])),
            ('pt', 30, np.array([0., 0.333, 0.])),
            ('pt', 31, np.array([0.5, 0., 0.833])),
            ('pt', 32, np.array([0.333, 0.333, 0.])),
            ('pt', 33, np.array([0.333, 0.333, 0.333])),
            ('pt', 34, np.array([0., 0.333, 0.])),
            ('pt', 35, np.array([0.833, 0.833, 0.])),
            ('pt', 36, np.array([0.333, 0., 0.])),
            ('pt', 37, np.array([0.333, 0., 0.])),
            ('pt', 38, np.array([0.333, 0., 0.])),
            ('pt', 39, np.array([0.583, 0.5, 0.25])),
        ]
        signals = []
        signal_index = 0
        for signal in op.run_strategies(steps=range(len(op.group_timing_table))):
            signals.append(signal)
            expected = signals_target[signal_index]
            print(signal, expected)
            self.assertEqual(expected[0], signal[0])
            self.assertEqual(expected[1], signal[1])
            self.assertTrue(np.allclose(expected[2], signal[2], atol=0.001))
            signal_index += 1

        print(f'Generating trading signals completed\n')
        self.assertEqual(len(signals), 40)
        self.assertEqual(len(signals), op.get_signal_count())
        self.assertEqual(len(signals), len(signals_target))

        # 5, set group_merge_type to 'and' and check signals
        op.group_merge_type = 'and'

        # create_data_windows()
        op.create_data_windows()
        print(f'Data Windows created for all strategies\n'
              f'Operator is ready: {op.is_ready(tell_me_why=True)}\n')
        # generate trading signals
        print(f'Generating trading signals with some ULCs = True with operator group_merge_type = "And"\n'
              f'Operator run schedule:\n'
              f'{op.group_timing_table}({len(op.group_timing_table)})\n'
              f'signal_count\n'
              f'{op.get_signal_count()}\n')
        # check that the signals are correct
        signals_target = [
            ('pt', 0, np.array([0., 0.333, 0.333])),
            ('pt', 1, np.array([0.333, 0.333, 0.333])),
            ('pt', 2, np.array([0., 0., 0.333])),
            ('pt', 3, np.array([0.1665, 0., 0.1665])),
            ('pt', 4, np.array([0.333, 0.333, 0.333])),
            ('pt', 5, np.array([0.333, 0.333, 0.333])),
            ('pt', 6, np.array([0., 0., 0.333])),
            ('pt', 7, np.array([0.08325, 0., 0.1665])),
            ('pt', 8, np.array([0.333, 0.333, 0.])),
            ('pt', 9, np.array([0.333, 0., 0.333])),
            ('pt', 10, np.array([0., 0., 0.333])),
            ('pt', 11, np.array([0., 0., 0.])),
            ('pt', 12, np.array([0., 0.333, 0.])),
            ('pt', 13, np.array([0., 0., 0.])),
            ('pt', 14, np.array([0.333, 0., 0.333])),
            ('pt', 15, np.array([0.08325, 0.08325, 0.1665])),
            ('pt', 16, np.array([0., 0.333, 0.])),
            ('pt', 17, np.array([0.333, 0.333, 0.333])),
            ('pt', 18, np.array([0.333, 0.333, 0.333])),
            ('pt', 19, np.array([0., 0., 0.])),
            ('pt', 20, np.array([0.333, 0.333, 0.])),
            ('pt', 21, np.array([0., 0.333, 0.333])),
            ('pt', 22, np.array([0.333, 0., 0.333])),
            ('pt', 23, np.array([0., 0., 0.])),
            ('pt', 24, np.array([0., 0.333, 0.])),
            ('pt', 25, np.array([0., 0.333, 0.])),
            ('pt', 26, np.array([0., 0.333, 0.333])),
            ('pt', 27, np.array([0., 0.1665, 0.])),
            ('pt', 28, np.array([0.333, 0.333, 0.333])),
            ('pt', 29, np.array([0.333, 0.333, 0.])),
            ('pt', 30, np.array([0., 0.333, 0.])),
            ('pt', 31, np.array([0., 0., 0.1665])),
            ('pt', 32, np.array([0.333, 0.333, 0.])),
            ('pt', 33, np.array([0.333, 0.333, 0.333])),
            ('pt', 34, np.array([0., 0.333, 0.])),
            ('pt', 35, np.array([0.1665, 0.1665, 0.])),
            ('pt', 36, np.array([0.333, 0., 0.])),
            ('pt', 37, np.array([0.333, 0., 0.])),
            ('pt', 38, np.array([0.333, 0., 0.])),
            ('pt', 39, np.array([0.08325, 0., 0.])),
        ]
        signals = []
        signal_index = 0
        for signal in op.run_strategies(steps=range(len(op.group_timing_table))):
            signals.append(signal)
            expected = signals_target[signal_index]
            print(signal, expected)
            self.assertEqual(expected[0], signal[0])
            self.assertEqual(expected[1], signal[1])
            self.assertTrue(np.allclose(expected[2], signal[2], atol=0.001))
            signal_index += 1

        print(f'Generating trading signals completed\n')
        self.assertEqual(len(signals), 40)
        self.assertEqual(len(signals), op.get_signal_count())
        self.assertEqual(len(signals), len(signals_target))

    def test_operator_signal_creation_using_reference_data(self):
        """测试operator对象运行交易策略生成交易信号过程中使用参考数据的情形"""
        # 创建一个测试交易策略，使用TestReferenceData类
        op = qt.Operator(strategies=[TestReferenceData], signal_type='PS')
        # operator只有一个strategy, set up blender
        op.set_group_parameters(group='Group_1', blender_str='s0')

        # create running schedule and prepare data buffer and data windows
        op.prepare_running_schedule(
                start_date='2023-01-12',
                end_date='2023-02-3',
                include_start_am=False,
                include_start_pm=False,
        )
        print(f'Operator running schedule prepared:\n'
              f'{op.group_timing_table}({len(op.group_timing_table)})\n')
        # prepare data buffer

        data_buffer = {
            'close_E_d':   close_d_df,
            'reference_d': close_ref_df,
        }
        op.prepare_data_buffer(
                start_date='2023-01-11',
                end_date='2023-01-31',
                data_package=data_buffer,
        )
        print(f'Operator data buffer prepared:\n')

        # create_data_windows()
        op.create_data_windows()
        print(f'Data Windows created for all strategies\n'
              f'Operator is ready: {op.is_ready(tell_me_why=True)}\n')

        for stg_id in op.strategy_ids:
            print(f'Strategy "{stg_id}" has data window for its data types:\n'
                  f'{op[stg_id].data_types}\n')
            for dtype in op[stg_id].data_types:
                data_window = op.data_window_views[stg_id][dtype]
                data_indices = op.data_window_indices[stg_id][dtype]
                print(f'Data type "{dtype}" has data window (shape: {data_window.shape}):\n'
                      f'{data_window[:3]}\n'
                      f'with window indices: {data_indices}\n'
                      )

        # check that the signals are correct
        signals_target = [
            ('ps', 0, np.array([0.5, 0.5, 0.])),
            ('ps', 1, np.array([0.5, 0., 0.5])),
            ('ps', 2, np.array([0.5, 0., 0.5])),
            ('ps', 3, np.array([0., 0.5, 0.5])),
            ('ps', 4, np.array([0.5, 0.5, 0.])),
            ('ps', 5, np.array([0.5, 0.5, 0.])),
            ('ps', 6, np.array([0.5, 0., 0.5])),
            ('ps', 7, np.array([0.5, 0., 0.5])),
            ('ps', 8, np.array([0.5, 0., 0.5])),
            ('ps', 9, np.array([0.5, 0.5, 0.])),
            ('ps', 10, np.array([0.5, 0.5, 0.])),
        ]
        signals = []
        signal_index = 0
        op.debug = True
        for signal in op.run_strategies(steps=range(len(op.group_timing_table))):
            signals.append(signal)
            expected = signals_target[signal_index]
            print(signal, expected)
            self.assertEqual(expected[0], signal[0])
            self.assertEqual(expected[1], signal[1])
            self.assertTrue(np.allclose(expected[2], signal[2], atol=0.001))
            signal_index += 1

    def test_operator_run_and_execute_static(self):
        """ 测试operator对象运行交易策略生成交易信号并执行交易信号更新持仓

        主要的目的是为了测试生成的交易信号能否被正确地执行，
        同时测试交易信号生成过程中需要用到交易数据的情形

        生成一个包含两个group，三个交易策略的operator对象，使用测试数据生成交易信号清单
        三个交易策略的运行频率固定，使用的交易数据固定，数据窗口长度固定，运行时间区间也固定
        交易信号生成后，送入backtest loop模块生成交易结果并更新交易持仓
        """

        # 创建一个测试交易策略，使用TestReferenceData类
        op = qt.Operator(strategies=[TestReferenceData], signal_type='PT')
        op.debug = True
        # operator只有一个strategy, set up blender
        op.set_group_parameters(group='Group_1', blender_str='s0')

        # create running schedule and prepare data buffer and data windows
        op.prepare_running_schedule(
                start_date='2023-01-12',
                end_date='2023-02-3',
                include_start_am=False,
                include_start_pm=False,
        )
        # prepare data buffer
        data_buffer = {
            'close_E_d':   close_d_df,
            'reference_d': close_ref_df,
        }
        op.prepare_data_buffer(
                start_date='2023-01-11',
                end_date='2023-01-31',
                data_package=data_buffer,
        )

        # 开始测试operator run/execute 使用batch模式（一次性创建所有交易信号并批量执行）
        op.create_data_windows()

        # 准备交易数据
        trade_price_data = trade_price_d_df.values[0:11]

        cash_investment_array = np.zeros((op.get_signal_count(),), dtype=float)
        cash_investment_array[0] = 1000000.0  # 初始资金
        cash_inflation_array = np.zeros((op.get_signal_count(),), dtype=float)
        delivery_day_indicators = np.ones((op.get_signal_count(),), dtype=bool)

        signal_parsing_params = dict(
                pt_buy_threshold=0.0,
                pt_sell_threshold=0.0,
                long_pos_limit=1.0,
                short_pos_limit=-1.0,
                allow_sell_short=False,
        )
        trading_moq_params = dict(
                moq_buy=10.,
                moq_sell=1.,
        )
        trading_delivery_params = dict(
                cash_delivery_period=0,
                stock_delivery_period=1,
        )

        backtested = Backtester(
                op=op,
                shares=list(close_d_df.columns),
                cash_plan=CashPlan(dates='20230112', amounts=[1000000.0], interest_rate=0.0),
                cash_inflation_array=cash_inflation_array,
                cash_investment_array=cash_investment_array,
                delivery_day_indicators=delivery_day_indicators,
                cost_params=np.array([0.0001, 0.0001, 5.0, 5.0, 0.]),
                signal_parsing_params=signal_parsing_params,
                trading_moq_params=trading_moq_params,
                trading_delivery_params=trading_delivery_params,
                trade_price_data=trade_price_data,
                benchmark_data=close_ref_df,
        ).run()

        with np.printoptions(suppress=True, precision=5):
            print(f'Backtest executed in batch mode, \n'
                  f'  op_run_time: {backtested.op_run_time:.6f}\n'
                  f'  backtest_time: {backtested.backtest_run_time:.6f}\n '
                  f'results:\n'
                  f'  own cashes: \n{backtested.own_cashes.round(3)}\n'
                  f'  available cashes: \n{backtested.available_cashes.round(3)}\n'
                  f'  own amounts: \n{backtested.own_amounts_array.round(2)}\n'
                  f'  available amounts: \n{backtested.available_amounts_array.round(2)}\n'
                  f'  trade records:\n{backtested.trade_records_array.round(2)}\n'
                  f'  trade costs:\n{backtested.trade_cost_array.round(3)}\n')

        target_own_cashes = np.array(
                [1000000., 236.034, 513221.953, 418.155, 510513.486, 550298.237,
                 353.248, 535421.976, 1165.663, 4513.043, 563167.275, 398.671],
        )
        target_available_cashes = np.array(
                [1000000., 236.034, 513221.953, 418.155, 510513.486, 550298.237,
                 353.248, 535421.976, 1165.663, 4513.043, 563167.275, 398.671]
        )
        target_own_stocks = np.array(
                [[0., 0., 0.],
                 [13300., 15380., 0.],
                 [13281., 0., 0.],
                 [13301., 0., 20880.],
                 [0., 10., 20880.],
                 [6580., 7490., 0.],
                 [13820., 15490., 0.],
                 [13767., 0., 10.],
                 [13743., 0., 21510.],
                 [13763., 0., 21349.],
                 [13763., 120., 0.],
                 [13853., 15890., 0.]]
        )
        target_available_stocks = np.array(
                [[0., 0., 0.],
                 [13300., 15380., 0.],
                 [13281., 0., 0.],
                 [13301., 0., 20880.],
                 [0., 10., 20880.],
                 [6580., 7490., 0.],
                 [13820., 15490., 0.],
                 [13767., 0., 10.],
                 [13743., 0., 21510.],
                 [13763., 0., 21349.],
                 [13763., 120., 0.],
                 [13853., 15890., 0.]],
        )
        target_trade_records = np.array(
                [[13300., 15380., 0.],
                 [-19., -15380., 0.],
                 [20., 0., 20880.],
                 [-13301., 10., 0.],
                 [6580., 7480., -20880.],
                 [7240., 8000., 0.],
                 [-53., -15490., 10.],
                 [-24., 0., 21500.],
                 [20., 0., -161.],
                 [0., 120., -21349.],
                 [90., 15770., 0.], ],
        )
        target_fees = np.array(
                [[49.981, 49.985, 0.],
                 [5., 51.231, 0.],
                 [5., 0., 51.198],
                 [51.049, 5., 0.],
                 [25.53, 25.499, 55.019],
                 [27.693, 27.296, 0.],
                 [5., 53.332, 5.],
                 [5., 0., 53.514],
                 [5., 0., 5.],
                 [0., 5., 56.297],
                 [5., 55.905, 0.]],
        )

        self.assertTrue(np.allclose(target_own_cashes, backtested.own_cashes, atol=0.01))
        self.assertTrue(np.allclose(target_available_cashes, backtested.available_cashes, atol=0.01))
        self.assertTrue(np.allclose(target_own_stocks, backtested.own_amounts_array))
        self.assertTrue(np.allclose(target_available_stocks, backtested.available_amounts_array))
        self.assertTrue(np.allclose(target_trade_records, backtested.trade_records_array))
        self.assertTrue(np.allclose(target_fees, backtested.trade_cost_array, atol=0.01))

    def test_operator_run_and_execute_dynamic(self):
        # 开始测试operator run/execute 使用stepwise模式（逐步创建交易信号并逐步回测，
        # 每次回测后更新operator的数据，因为operator的运行依赖回测数据）

        # 初始化一个新的operator对象，生成一个需要依赖性数据的交易策略
        # 创建一个测试交易策略，使用TestReferenceData类
        op = qt.Operator(strategies=[TestDependentData], signal_type='PT')
        # operator只有一个strategy, set up blender
        op.set_group_parameters(group='Group_1', blender_str='s0')

        # create running schedule and prepare data buffer and data windows
        op.prepare_running_schedule(
                start_date='2023-01-12',
                end_date='2023-02-3',
                include_start_am=False,
                include_start_pm=False,
        )
        # prepare data buffer
        data_buffer = {
            'close_E_d':            close_d_df,
            'reference_d':          close_ref_df,
            'op_holding_positions': None,
            'op_trade_prices':      None,
            'op_trade_volumes':     None,
            'op_cashes':            None,
        }
        op.prepare_data_buffer(
                start_date='2023-01-11',
                end_date='2023-01-31',
                data_package=data_buffer,
        )
        # 开始测试operator run/execute 使用stepwise模式（生成一组交易信号并生成一组结果，循环执行直到生成全部结果）
        op.create_data_windows()

        # 准备交易数据
        trade_price_data = trade_price_d_df.values[0:11]

        cash_investment_array = np.zeros((op.get_signal_count(),), dtype=float)
        cash_investment_array[0] = 1000000.0  # 初始资金
        cash_inflation_array = np.zeros((op.get_signal_count(),), dtype=float)
        delivery_day_indicators = np.ones((op.get_signal_count(),), dtype=bool)

        signal_parsing_params = dict(
                pt_buy_threshold=0.0,
                pt_sell_threshold=0.0,
                long_pos_limit=1.0,
                short_pos_limit=-1.0,
                allow_sell_short=False,
        )
        trading_moq_params = dict(
                moq_buy=10.,
                moq_sell=1.,
        )
        trading_delivery_params = dict(
                cash_delivery_period=0,
                stock_delivery_period=1,
        )

        backtested = Backtester(
                op=op,
                shares=list(close_d_df.columns),
                cash_plan=CashPlan(dates='20230112', amounts=[1000000.0], interest_rate=0.0),
                cash_inflation_array=cash_inflation_array,
                cash_investment_array=cash_investment_array,
                delivery_day_indicators=delivery_day_indicators,
                cost_params=np.array([0.0001, 0.0001, 5.0, 5.0, 0.]),
                signal_parsing_params=signal_parsing_params,
                trading_moq_params=trading_moq_params,
                trading_delivery_params=trading_delivery_params,
                trade_price_data=trade_price_data,
                benchmark_data=close_ref_df,
        ).run()

        with np.printoptions(suppress=True, precision=5):
            print(f'Backtest executed in stepwise mode, \n'
                  f'  op_run_time: {backtested.op_run_time:.6f}\n'
                  f'  backtest_time: {backtested.backtest_run_time:.6f}\n'
                  f'results:\n'
                  f'  own cashes: \n{backtested.own_cashes.round(3)}\n'
                  f'  available cashes: \n{backtested.available_cashes.round(3)}\n'
                  f'  own amounts: \n{backtested.own_amounts_array.round(2)}\n'
                  f'  available amounts: \n{backtested.available_amounts_array.round(2)}\n'
                  f'  trade records:\n{backtested.trade_records_array.round(2)}\n'
                  f'  trade costs:\n{backtested.trade_cost_array.round(3)}\n')

        target_own_cashes = np.array(
                [1000000., 236.034, 513221.953, 418.155, 510513.486, 550298.237,
                 353.248, 535421.976, 1165.663, 4513.043, 563167.275, 398.671],
        )
        target_available_cashes = np.array(
                [1000000., 236.034, 513221.953, 418.155, 510513.486, 550298.237,
                 353.248, 535421.976, 1165.663, 4513.043, 563167.275, 398.671]
        )
        target_own_stocks = np.array(
                [[0., 0., 0.],
                 [13300., 15380., 0.],
                 [13281., 0., 0.],
                 [13301., 0., 20880.],
                 [0., 10., 20880.],
                 [6580., 7490., 0.],
                 [13820., 15490., 0.],
                 [13767., 0., 10.],
                 [13743., 0., 21510.],
                 [13763., 0., 21349.],
                 [13763., 120., 0.],
                 [13853., 15890., 0.]]
        )
        target_available_stocks = np.array(
                [[0., 0., 0.],
                 [13300., 15380., 0.],
                 [13281., 0., 0.],
                 [13301., 0., 20880.],
                 [0., 10., 20880.],
                 [6580., 7490., 0.],
                 [13820., 15490., 0.],
                 [13767., 0., 10.],
                 [13743., 0., 21510.],
                 [13763., 0., 21349.],
                 [13763., 120., 0.],
                 [13853., 15890., 0.]],
        )
        target_trade_records = np.array(
                [[13300., 15380., 0.],
                 [-19., - 15380., 0.],
                 [20., 0., 20880.],
                 [-13301., 10., 0.],
                 [6580., 7480., - 20880.],
                 [7240., 8000., 0.],
                 [-53., - 15490., 10.],
                 [-24., 0., 21500.],
                 [20., 0., - 161.],
                 [0., 120., - 21349.],
                 [90., 15770., 0.]],
        )
        target_fees = np.array(
                [[49.981, 49.985, 0.],
                 [5., 51.231, 0.],
                 [5., 0., 51.198],
                 [51.049, 5., 0.],
                 [25.53, 25.499, 55.019],
                 [27.693, 27.296, 0.],
                 [5., 53.332, 5.],
                 [5., 0., 53.514],
                 [5., 0., 5.],
                 [0., 5., 56.297],
                 [5., 55.905, 0.]],
        )

        self.assertTrue(np.allclose(target_own_cashes, backtested.own_cashes, atol=0.01))
        self.assertTrue(np.allclose(target_available_cashes, backtested.available_cashes, atol=0.01))
        self.assertTrue(np.allclose(target_own_stocks, backtested.own_amounts_array))
        self.assertTrue(np.allclose(target_available_stocks, backtested.available_amounts_array))
        self.assertTrue(np.allclose(target_trade_records, backtested.trade_records_array))
        self.assertTrue(np.allclose(target_fees, backtested.trade_cost_array, atol=0.01))

    def test_operator_run_and_execute_with_large_data_set_for_speed(self):
        """ 使用一个随机生成的超大数据集(近7万行，1000列）测试operator对象运行交易策略生成交易信号并执行交易信号更新持仓

        使用同样的数据集重复运行十次以测试运行速度（测试速度前先运行一次以完成函数的namba编译）
        """
        # 创建一个测试交易策略，使用TestReferenceData类
        op = qt.Operator(strategies=[TestReferenceData], signal_type='VS')
        # 设置交易策略运行频率为5min，开始日期2020-01-01，结束日期2025-12-31，共69792行
        op.set_parameter(stg_id='custom', run_freq='5min')
        # operator只有一个strategy, set up blender
        op.set_group_parameters(group='Group_1', blender_str='s0')

        # create running schedule and prepare data buffer and data windows
        op.prepare_running_schedule(
                start_date='2020-01-15',
                # end_date='2020-06-30',
                end_date='2025-12-31',
                include_start_am=False,
                include_start_pm=False,
        )
        print(f'Operator running schedule prepared:\n'
              f'total steps = {len(op.group_timing_table)}\n')

        # 准备一个超大的数据集
        share_count = 1000
        shares = [f's_{i}' for i in range(share_count)]

        large_close = pd.DataFrame(
                np.random.randint(10, size=(69792, share_count)).astype(float) + 30,
                columns=[f's_{i}' for i in range(share_count)],
                index=tti(start='2020-01-01', end='2025-12-31', freq='5min', include_start_am=False,
                          include_start_pm=False)
        )
        large_ref = pd.DataFrame(
                np.random.randint(10, size=(1477, 1)).astype(float) + 100,
                columns=['ref'],
                index=tti(start='2019-12-01', end='2025-12-31', freq='d', include_start_am=False,
                          include_start_pm=False)
        )['ref']  # reference data is a pd.DataFrame
        data_buffer = {
            'close_E_d':   large_close,
            'reference_d': large_ref,
        }
        op.prepare_data_buffer(
                start_date='2020-01-03',
                end_date='2025-12-31',
                # end_date='2020-06-30',
                data_package=data_buffer,
        )

        # 准备大量交易价格数据

        signal_count = op.get_signal_count()

        trade_price_data = pd.DataFrame(
                np.random.randint(10, size=(signal_count, share_count)).astype(float) + 30,
                columns=[f's_{i}' for i in range(share_count)],
                index=tti(start='2020-01-15', end='2025-12-31', freq='5min', include_start_am=False,
                          include_start_pm=False)
        )

        benchmark_data = pd.Series(
                np.random.randint(10, size=(signal_count,)).astype(float) + 30,
                index=tti(start='2020-01-15', end='2025-12-31', freq='5min', include_start_am=False,
                          include_start_pm=False)
        )
        print(f'created random trade price data of shape {trade_price_data.shape}\n'
              f'starting from {trade_price_data.index[0]} to {trade_price_data.index[-1]}\n,'
              f'prepared data buffer with large data set of shape: {large_close.shape}\n'
              f'starting from {large_close.index[0]} to {large_close.index[-1]}\n'
              f'prepared reference data of shape: {large_ref.shape}\n'
              f'starting from {large_ref.index[0]} to {large_ref.index[-1]}\n')

        trade_price_data = trade_price_data.values

        # 开始测试operator run/execute 使用batch模式（一次性创建所有交易信号并批量执行）
        op.create_data_windows()
        # 设置股票名称
        op.set_shares([f's_{i}' for i in range(share_count)])

        # 准备交易数据
        cash_investment_array = np.zeros((signal_count,), dtype=float)
        cash_investment_array[0] = 1000000.0  # 初始资金
        cash_inflation_array = np.zeros((signal_count,), dtype=float)
        delivery_day_indicators = np.ones((signal_count,), dtype=bool)

        cost_params = np.array([0.0001, 0.0001, 5.0, 5.0, 0.0])
        signal_parsing_params = dict(
                pt_buy_threshold=0.0,
                pt_sell_threshold=0.0,
                long_pos_limit=1.0,
                short_pos_limit=-1.0,
                allow_sell_short=True,
        )
        trading_moq_params = dict(
                moq_buy=10.,
                moq_sell=1.,
        )
        trading_delivery_params = dict(
                cash_delivery_period=0,
                stock_delivery_period=1,
        )

        # 使用Backtester对象执行回测并计算回测时间
        backtested = Backtester(
                op=op,
                shares=shares,
                cash_plan=CashPlan(dates=['2020-01-15'], amounts=[1000000.0], interest_rate=0.0),
                cash_investment_array=cash_investment_array,
                cash_inflation_array=cash_inflation_array,
                delivery_day_indicators=delivery_day_indicators,
                cost_params=cost_params,
                signal_parsing_params=signal_parsing_params,
                trading_moq_params=trading_moq_params,
                trading_delivery_params=trading_delivery_params,
                trade_price_data=trade_price_data,
                benchmark_data=benchmark_data,
        ).run()

        signal_time = backtested.op_run_time
        print(f'Generated {op.get_signal_count()} trading signals in {signal_time:.6f} seconds\n')
        self.assertLessEqual(signal_time, 8.0)  # 交易信号生成时间不超过8秒
        backtest_time = backtested.backtest_run_time
        print(f'Backtested {op.get_signal_count()} and created trading tables in {backtest_time:.6f} seconds\n')
        self.assertLessEqual(backtest_time, 8.0)  # 回测时间不超过8秒

        # 测试计算回测最终结果的时间（包括计算三种回测分析结果）
        st = time.time()
        final_value = backtested.trade_result_final_value()
        mdd = backtested.trade_result_max_drawdown()
        volatility = backtested.trade_result_volatility()
        et = time.time()
        print(f'Calculated backtest numaric results: \n'
              f' - fv:          {final_value:.3f}\n'
              f' - mdd:         {mdd:.2%}\n'
              f' - volatility:  {volatility:.3f}\n'
              f' in {et - st:.6f} seconds\n')
        self.assertLessEqual(et - st, 5.0)  # 生成净值记录时间不超过5秒

        # 测试生成回测记录的完整评价指标并计算时间：
        st = time.time()
        backtested.evaluate_result(indicators='r,m,v,b,info,alpha,beta,sharp')
        et = time.time()
        print(f'Complete evaluation of backtest results took {et - st:.6f} seconds\n')
        self.assertLessEqual(et - st, 5.0)  # 生成完整评价记录时间不超过5秒

        # 测试生成基本交易结果DataFrame的时间
        st = time.time()
        result_df = backtested.trade_result_df()
        et = time.time()
        print(f'Created basic trade result dataframe with {len(result_df)} records in {et - st:.6f} seconds\n'
              f'trade log:\n{result_df.head()}\n')
        self.assertLessEqual(et - st, 5.0)  # 生成交易日志时间不超过5秒

        # 测试生成完整交易日志DataFrame的时间
        st = time.time()
        trade_log = backtested.generate_trade_logs()
        et = time.time()
        print(f'Created trade log:\n{trade_log.head()}\n in {et - st:.6f} seconds\n')

        # 测试生成完整交易汇总表DataFrame的时间
        st = time.time()
        trade_summary = backtested.generate_trade_summary()
        et = time.time()
        print(f'Created trade summary:\n{trade_summary.head()}\n in {et - st:.6f} seconds\n')

        # 重复运行十次以精确测算回测的速度
        print(f'Now repeating the backtest 10 times to measure speed...\n')
        op_run_times = []
        backtest_times = []
        cycles = 10
        from tqdm import trange
        for _ in trange(cycles):
            backtested.run()
            op_run_times.append(backtested.op_run_time)
            backtest_times.append(backtested.backtest_run_time)

        op_run_time = sum(op_run_times)
        backtest_time = sum(backtest_times)
        print(f'10 times backtest executed for {op.get_signal_count()} signals in: \n'
              f' - signal creation (total/avg):  {op_run_time:.3f} / {(op_run_time / cycles):.3f} sec\n'
              f' - backtest (total/avg)          {backtest_time:.3f} / {(backtest_time / cycles):.3f} sec')
        self.assertLessEqual((et - st) / 5, 10.0)  # 单次平均回测时间不超过10秒

    def test_non_day_data_freqs(self):
        """测试除d之外的其他数据频率交易策略"""
        # TODO: implement this function
        pass

    def test_long_short_position_limits(self):
        """ 测试多头和空头仓位的最高仓位限制 """
        # TODO: implement this function
        pass

    def test_check_and_prepare_data(self):
        """ 测试函数check_and_prepare_backtest_data() in core.py"""

        op = qt.Operator()
        op.add_strategies([TestGenStg, TestFactorSorter])  # first group: d@close
        op.add_strategies([TestRuleIter], run_freq='h')  # second group: h@close
        print(f'Operator created with {op.strategy_group_count} groups of strategies:\n')
        op.set_parameter('custom', use_latest_data_cycle=False, par_values=(4, 0.5))
        op.set_parameter('custom_1', use_latest_data_cycle=False, par_values=(3, 0.5), max_sel_count=2,
                         weighting='even')
        op.set_parameter('custom_2', use_latest_data_cycle=False, par_values=('option1', np.array([1, 2, 3])))
        op.info(verbose=True)

        # set up blender
        op.set_group_parameters(group='Group_1', blender_str='(s0+s1)/2')
        op.set_group_parameters(group='Group_2', blender_str='s0')
        op.group_merge_type = 'None'

        # create running schedule and prepare data buffer and data windows
        op.prepare_running_schedule(
                start_date='2023-01-10',
                end_date='2023-01-31',
                include_start_am=False,
                include_start_pm=False,
        )
        print(f'Operator running schedule prepared:\n')


class TestAddStrategy(unittest.TestCase):
    """测试Operator类的add_strategy方法"""

    def setUp(self):
        """测试前准备"""
        self.operator = qt.Operator()

    def test_add_strategy_with_string_builtin(self):
        """测试添加内置策略字符串"""
        # 添加策略
        self.operator.add_strategy('dma', opt_tag=1, par_values=(50, 10, 20))

        # 验证策略被正确添加
        self.assertEqual(len(self.operator.strategies), 1)
        self.assertIsInstance(self.operator.strategies[0], DMA)
        # 验证参数被设置
        self.assertEqual(self.operator.strategies[0].opt_tag, 1)
        self.assertEqual(self.operator.strategies[0].par_values, (50, 10, 20))

    def test_add_strategy_with_base_strategy_object(self):
        """测试添加BaseStrategy对象"""
        # 创建策略对象
        strategy = DMA()

        # 添加策略
        self.operator.add_strategy(strategy)

        # 验证策略被正确添加
        self.assertEqual(len(self.operator.strategies), 1)
        self.assertIs(self.operator.strategies[0], strategy)
        self.assertEqual(self.operator.strategies[0].strategy_id, 'dma')

    def test_add_strategy_with_strategy_type(self):
        """测试添加策略类型"""
        # 添加策略类型
        self.operator.add_strategy(DMA)

        # 验证策略被正确添加
        self.assertEqual(len(self.operator.strategies), 1)
        self.assertIsInstance(self.operator.strategies[0], DMA)
        self.assertEqual(self.operator.strategies[0].strategy_id, 'dma')

    def test_add_strategy_with_tuple_raises_error(self):
        """测试传入元组时抛出异常"""
        with self.assertRaises(TypeError) as context:
            self.operator.add_strategy(('strategy1', 'strategy2'))

        self.assertIn('Strategy can not be a tuple of a list', str(context.exception))

    def test_add_strategy_with_list_raises_error(self):
        """测试传入列表时抛出异常"""
        with self.assertRaises(TypeError) as context:
            self.operator.add_strategy(['strategy1', 'strategy2'])

        self.assertIn('Strategy can not be a tuple of a list', str(context.exception))

    def test_add_strategy_with_unsupported_type_raises_error(self):
        """测试传入不支持类型时抛出异常"""
        with self.assertRaises(TypeError) as context:
            self.operator.add_strategy(123)

        self.assertIn('The strategy type', str(context.exception))

    def test_add_duplicate_strategy(self):
        """测试添加重复策略时抛出异常"""
        self.operator.clear_strategies()
        # 第一次添加成功
        self.operator.add_strategy('dma')

        # 第二次添加相同策略产生第二个策略ID
        self.operator.add_strategy('dma')

        self.assertEqual(self.operator.strategy_count, 2)
        self.assertEqual(self.operator[0].strategy_id, 'dma')
        self.assertEqual(self.operator[1].strategy_id, 'dma_1')

    def test_add_strategy_with_run_freq_and_timing(self):
        """测试添加策略时设置run_freq和run_timing参数"""
        # 添加策略并设置特殊参数
        self.operator.add_strategy('dma', run_freq='d', run_timing='open')

        # 验证参数被正确设置
        self.assertEqual(self.operator.strategies[0].run_freq, 'd')
        self.assertEqual(self.operator.strategies[0].run_timing, 'open')

    def test_add_strategy_with_invalid_run_freq_type_raises_error(self):
        """测试传入无效run_freq类型时抛出异常"""
        with self.assertRaises(TypeError) as context:
            self.operator.add_strategy('dma', run_freq=123)

        self.assertIn('run_freq should be a string', str(context.exception))

    def test_add_strategy_with_invalid_run_timing_type_raises_error(self):
        """测试传入无效run_timing类型时抛出异常"""
        with self.assertRaises(TypeError) as context:
            self.operator.add_strategy('dma', run_timing=123)

        self.assertIn('run_timing should be a string', str(context.exception))

    def test_add_strategy_creates_new_group(self):
        """测试添加策略时创建新分组"""
        # 添加策略
        self.operator.add_strategy('dma')

        # 验证创建了新分组
        self.assertEqual(len(self.operator._groups), 1)
        self.assertEqual(self.operator._groups[0].run_freq, 'd')
        self.assertEqual(self.operator._groups[0].run_timing, 'close')

    def test_add_strategy_to_existing_group(self):
        """测试将策略添加到现有分组"""
        # 添加第一个策略
        self.operator.add_strategy('dma')

        # 添加第二个策略（相同频率和时机）
        self.operator.add_strategy('macd')

        # 验证两个策略在同一个分组中
        self.assertEqual(len(self.operator._groups), 1)
        self.assertEqual(len(self.operator._groups[0].members), 2)

    def test_add_strategy_calls_set_parameter(self):
        """测试添加策略时调用set_parameter方法"""
        # 添加策略并设置参数
        self.operator.add_strategy('dma', opt_tag=1, par_values=(50, 10, 20))

        # 验证参数被设置
        self.assertEqual(self.operator.strategies[0].opt_tag, 1)
        self.assertEqual(self.operator.strategies[0].par_values, (50, 10, 20))

    def test_add_strategy_with_multiple_kwargs(self):
        """测试添加策略时传入多个参数"""
        # 添加策略并设置多个参数
        self.operator.add_strategy('dma',
                                   opt_tag=1,
                                   par_values=(50, 10, 20),
                                   run_freq='d',
                                   run_timing='open',
                                   window_length=30)

        # 验证策略被正确添加
        self.assertEqual(len(self.operator.strategies), 1)
        # 验证参数被设置
        self.assertEqual(self.operator.strategies[0].run_freq, 'd')
        self.assertEqual(self.operator.strategies[0].run_timing, 'open')
        self.assertEqual(self.operator.strategies[0].opt_tag, 1)

    def test_add_strategy_with_none_run_freq_and_timing(self):
        """测试传入None值的run_freq和run_timing参数"""
        # 创建策略对象
        strategy = DMA()

        # 添加策略并设置None参数
        self.operator.add_strategy(strategy)

        # 验证策略的原始值被保留
        self.assertEqual(strategy.run_freq, 'd')
        self.assertEqual(strategy.run_timing, 'close')

    def test_add_strategy_id_generation(self):
        """测试策略ID生成机制"""
        # 添加第一个策略
        self.operator.add_strategy('dma')
        self.assertIn('dma', self.operator.strategy_ids[0])

        # 添加第二个同名策略
        self.operator.add_strategy('dma')
        self.assertIn('dma', self.operator.strategy_ids[1])
        self.assertNotEqual(self.operator.strategy_ids[0], self.operator.strategy_ids[1])

    def test_add_strategy_group_creation_logic(self):
        """测试策略分组创建逻辑"""
        # 添加第一个策略
        self.operator.add_strategy('dma', run_freq='d', run_timing='open')
        first_group_id = self.operator._groups[0].name

        # 添加具有不同运行频率的策略
        self.operator.add_strategy('macd', run_freq='h', run_timing='open')
        second_group_id = self.operator._groups[1].name

        # 验证创建了两个不同的分组
        self.assertEqual(len(self.operator._groups), 2)
        self.assertNotEqual(first_group_id, second_group_id)

        # 添加具有相同运行频率和时机的策略
        self.operator.add_strategy('dma', run_freq='d', run_timing='open')

        # 验证第三个策略被添加到第一个分组
        self.assertEqual(len(self.operator._groups), 2)
        self.assertEqual(len(self.operator._groups[0].members), 2)

    def test_add_strategy_with_custom_strategy_object(self):
        """测试添加自定义策略对象"""

        class CustomStrategy(qt.GeneralStg):
            def __init__(self):
                super().__init__()
                self._strategy_id = None

        custom_strategy = CustomStrategy()

        # 添加自定义策略
        self.operator.add_strategy(custom_strategy)

        # 验证策略被正确添加
        self.assertEqual(len(self.operator.strategies), 1)
        self.assertIs(self.operator.strategies[0], custom_strategy)
        self.assertEqual(custom_strategy._strategy_id, 'custom')

    def test_add_strategy_with_strategy_class(self):
        """测试添加策略类"""

        class CustomStrategyClass(qt.GeneralStg):
            def __init__(self):
                super().__init__()
                self._strategy_id = None

        # 添加策略类
        self.operator.add_strategy(CustomStrategyClass)

        # 验证策略实例被正确创建和添加
        self.assertEqual(len(self.operator.strategies), 1)
        self.assertIsInstance(self.operator.strategies[0], CustomStrategyClass)
        self.assertEqual(self.operator.strategies[0]._strategy_id, 'custom')

    def test_add_strategy_with_empty_operator(self):
        """测试在空Operator中添加策略"""
        # 验证初始状态
        self.assertEqual(self.operator.strategy_count, 0)
        self.assertEqual(len(self.operator._groups), 0)

        # 添加策略
        class MockStrategy(qt.GeneralStg):
            def __init__(self):
                super().__init__()
                self._strategy_id = None

        self.operator.add_strategy(MockStrategy)

        # 验证添加后状态
        self.assertEqual(self.operator.strategy_count, 1)
        self.assertEqual(len(self.operator._groups), 1)
        self.assertEqual(len(self.operator._groups[0].members), 1)

    def test_add_strategy_parameter_setting_integration(self):
        """测试添加策略与参数设置的集成"""
        # 添加策略并设置多个参数
        self.operator.add_strategy('dma',
                                   opt_tag=2,
                                   par_values=(10, 20, 30),
                                   window_length=50)

        # 验证参数被设置
        strategy = self.operator.strategies[0]
        self.assertEqual(strategy.opt_tag, 2)
        self.assertEqual(strategy.par_values, (10, 20, 30))
        self.assertEqual(strategy.window_lengths['close_ANY_d'], 50)

    def test_add_strategy_different_timing_and_freq(self):
        """测试添加不同运行时机和频率的策略"""
        # 添加默认策略
        self.operator.add_strategy('dma')
        self.assertEqual(len(self.operator._groups), 1)
        self.assertEqual(self.operator._groups[0].run_freq, 'd')
        self.assertEqual(self.operator._groups[0].run_timing, 'close')

        # 添加不同频率的策略
        self.operator.add_strategy('macd', run_freq='h', run_timing='close')
        self.assertEqual(len(self.operator._groups), 2)

        # 添加不同时机的策略
        self.operator.add_strategy('dma', run_freq='d', run_timing='open')
        self.assertEqual(len(self.operator._groups), 3)

    def test_add_strategy_custom_strategy_with_params(self):
        """测试添加带参数的自定义策略"""

        class CustomStrategy(qt.GeneralStg):
            def __init__(self, **kwargs):
                super().__init__(**kwargs)
                self._strategy_id = None

        custom_strategy = CustomStrategy(opt_tag=1, par_values=(5, 10),
                                         pars=[Parameter(par_range=(0, 50), name='par_1', par_type='int'),
                                               Parameter(par_range=(0, 50), name='par_2', par_type='int')])

        self.operator.add_strategy(custom_strategy)

        self.assertEqual(len(self.operator.strategies), 1)
        self.assertEqual(custom_strategy.opt_tag, 1)
        self.assertEqual(custom_strategy.par_values, (5, 10))

    def test_add_strategy_group_assignment(self):
        """测试策略正确分配到分组"""
        # 添加第一个策略
        self.operator.add_strategy('dma', run_freq='d', run_timing='close')

        # 添加第二个相同运行参数的策略
        self.operator.add_strategy('macd', run_freq='d', run_timing='close')

        # 添加第三个不同运行参数的策略
        self.operator.add_strategy('dma', run_freq='h', run_timing='close')

        # 验证分组情况
        self.assertEqual(len(self.operator._groups), 2)
        self.assertEqual(len(self.operator._groups[0].members), 2)  # 第一组有两个策略
        self.assertEqual(len(self.operator._groups[1].members), 1)  # 第二组有一个策略

    def test_add_strategy_with_all_parameter_types(self):
        """测试添加策略时传入各种类型的参数"""
        # 添加策略并设置各种参数
        self.operator.add_strategy('dma',
                                   opt_tag=1,
                                   par_values=(10, 20, 30),
                                   run_freq='d',
                                   run_timing='open',
                                   window_length=60,
                                   use_latest_data_cycle=True,
                                   data_types={'close_ANY_d': qt.DataType('close', freq='d', asset_type='ANY')})

        # 验证参数被设置
        strategy = self.operator.strategies[0]
        self.assertEqual(strategy.opt_tag, 1)
        self.assertEqual(strategy.par_values, (10, 20, 30))
        self.assertEqual(strategy.run_freq, 'd')
        self.assertEqual(strategy.run_timing, 'open')
        self.assertEqual(strategy.window_lengths['close_ANY_d'], 60)
        self.assertEqual(strategy.data_ulc['close_ANY_d'], True)

    def test_add_strategy_duplicate_check(self):
        """测试策略重复检查功能"""
        # 添加策略
        strategy1 = DMA()
        strategy2 = DMA()

        self.operator.add_strategy(strategy1)

        # 尝试添加相同的策略实例
        with self.assertRaises(ValueError):
            self.operator.add_strategy(strategy1)

        # 添加不同的策略实例，但相同的类型
        self.operator.add_strategy(strategy2)
        self.assertEqual(len(self.operator.strategies), 2)


class TestSetBlender(unittest.TestCase):
    """测试Operator类的set_blender方法"""

    def setUp(self):
        """设置测试环境"""
        self.operator = qt.Operator()

    def test_strategy_count_zero(self):
        """测试策略数量为0时直接返回"""
        # 创建一个没有策略的Operator
        operator_empty = qt.Operator()
        # 验证策略数量为0
        self.assertEqual(operator_empty.strategy_count, 0)
        # 调用set_blender方法，应该直接返回，不执行任何操作
        result = operator_empty.set_blender('s0+s1', 'Group_1')
        self.assertIsNone(result)

    def test_group_id_none_blender_none(self):
        """测试group_id为None且blender为None时直接返回"""
        self.operator.add_strategy('dma')
        self.assertEqual(self.operator.strategy_count, 1)
        result = self.operator.set_blender(None, None)
        self.assertIsNone(result)

    def test_group_id_none_blender_string(self):
        """测试group_id为None且blender为字符串时转换为列表处理"""
        self.operator.add_strategies('dma, trix')
        self.operator.add_strategies('macd, ddema', run_freq='h', run_timing='open')

        # 验证有两个策略组
        self.assertEqual(self.operator.strategy_group_count, 2)

        # 设置blender
        self.operator.set_blender('s0+s1', None)

        # 验证两个组都被设置为相同的blender
        group1 = self.operator.strategy_groups['Group_1']
        group2 = self.operator.strategy_groups['Group_2']
        self.assertEqual(group1.blender_str, 's0+s1')
        self.assertEqual(group2.blender_str, 's0+s1')

        print(self.operator.get_blender())
        print(self.operator.get_blender('Group_1'))
        print(self.operator.get_blender('Group_2'))
        self.assertIs(self.operator.get_blender('Not existed'), None)

        print(self.operator.view_blender())
        print(self.operator.view_blender('Group_1'))
        print(self.operator.view_blender('Group_2'))
        self.assertRaises(KeyError, self.operator.view_blender, 0)
        self.assertRaises(KeyError, self.operator.view_blender, 'not_existed')

    def test_group_id_none_blender_empty_list(self):
        """测试blender为空列表时抛出ValueError"""
        self.operator.add_strategy('dma')
        with self.assertRaises(ValueError) as context:
            self.operator.set_blender([], None)
        self.assertIn('Empty blender list!', str(context.exception))

    def test_group_id_none_blender_list_with_non_string(self):
        """测试blender列表包含非字符串元素时抛出TypeError"""
        self.operator.add_strategy('dma')
        self.operator.add_strategy('macd', run_freq='h', run_timing='open')
        with self.assertRaises(TypeError) as context:
            self.operator.set_blender(['s0+s1', 123], None)
        self.assertIn('All items in blender list should be strings!', str(context.exception))

    def test_group_id_none_blender_list_insufficient(self):
        """测试blender列表数量不足时用最后一个元素补齐"""
        self.operator.add_strategy('dma')
        self.operator.add_strategy('macd', run_freq='h', run_timing='open')
        self.operator.add_strategy('dma', run_freq='w', run_timing='close')

        # 验证有3个策略组
        self.assertEqual(self.operator.strategy_group_count, 3)

        # 设置只有1个blender值的列表，应该被扩展到3个
        self.operator.set_blender(['s0+s1'], None)

        # 验证所有组都被设置为最后一个元素的值
        groups = self.operator.strategy_groups
        self.assertEqual(groups['Group_1'].blender_str, 's0+s1')
        self.assertEqual(groups['Group_2'].blender_str, 's0+s1')
        self.assertEqual(groups['Group_3'].blender_str, 's0+s1')

    def test_group_id_none_blender_list_sufficient(self):
        """测试blender列表数量足够时正常分配"""
        self.operator.add_strategy('dma')
        self.operator.add_strategy('macd', run_freq='h', run_timing='open')

        # 设置与策略组数量相等的blender列表
        self.operator.set_blender(['s0+s1', 's2*s3'], None)

        # 验证每个组被分配对应的blender
        groups = self.operator.strategy_groups
        self.assertEqual(groups['Group_1'].blender_str, 's0+s1')
        self.assertEqual(groups['Group_2'].blender_str, 's2*s3')

    def test_group_id_none_blender_list_excessive(self):
        """测试blender列表数量过多时只使用前面的元素"""
        self.operator.add_strategy('dma')
        self.operator.add_strategy('macd', run_freq='h', run_timing='open')

        # 设置超过策略组数量的blender列表
        self.operator.set_blender(['s0+s1', 's2*s3', 's1-s0'], None)

        # 验证只有前两个被使用
        groups = self.operator.strategy_groups
        self.assertEqual(groups['Group_1'].blender_str, 's0+s1')
        self.assertEqual(groups['Group_2'].blender_str, 's2*s3')

    def test_group_id_none_blender_dict(self):
        """测试blender为字典时遍历设置"""
        self.operator.add_strategy('dma')
        self.operator.add_strategy('macd', run_freq='h', run_timing='open')

        # 设置字典形式的blender
        blender_dict = {'Group_1': 's0+s1', 'Group_2': 's2*s3'}
        self.operator.set_blender(blender_dict, None)

        # 验证每个组被设置为字典中对应的值
        groups = self.operator.strategy_groups
        self.assertEqual(groups['Group_1'].blender_str, 's0+s1')
        self.assertEqual(groups['Group_2'].blender_str, 's2*s3')

    def test_group_id_none_blender_invalid_type(self):
        """测试blender为无效类型时抛出TypeError"""
        self.operator.add_strategy('dma')
        with self.assertRaises(TypeError) as context:
            self.operator.set_blender(123, None)
        self.assertIn('Wrong type of blender', str(context.exception))

    def test_group_id_valid_blender_string(self):
        """测试group_id有效且blender为字符串时正常设置"""
        self.operator.add_strategy('dma')
        self.operator.add_strategy('macd', run_freq='h', run_timing='open')

        # 验证blender_str被正确设置
        self.operator.set_blender('s0+s1', 'Group_1')
        self.assertEqual(self.operator.strategy_groups['Group_1'].blender_str, 's0+s1')

    def test_group_id_invalid(self):
        """测试group_id无效时抛出KeyError"""
        self.operator.add_strategy('dma')
        with self.assertRaises(KeyError) as context:
            self.operator.set_blender('s0+s1', 'Invalid_Group')
        self.assertIn("Strategy group 'Invalid_Group' is not valid", str(context.exception))

    def test_group_id_valid_blender_invalid_type(self):
        """测试group_id有效但blender类型错误时抛出TypeError"""
        self.operator.add_strategy('dma')
        with self.assertRaises(TypeError) as context:
            self.operator.set_blender(123, 'Group_1')
        self.assertIn('Wrong type of blender', str(context.exception))

    def test_group_id_invalid_type(self):
        """测试group_id类型错误时抛出TypeError"""
        self.operator.add_strategy('dma')
        with self.assertRaises(TypeError) as context:
            self.operator.set_blender('s0+s1', 123)
        self.assertIn('group should be a string', str(context.exception))

    def test_invalid_blender_expression(self):
        """测试无效blender表达式时抛出ValueError"""
        self.operator.add_strategy('dma')
        with self.assertRaises(ValueError) as context:
            self.operator.set_blender('invalid@expr', 'Group_1')
        # 注意：这里可能不会抛出异常，因为blender_str属性设置可能不会立即验证表达式
        # 但如果表达式无效，应该在Group类中被验证

    def test_blender_with_complex_expression(self):
        """测试复杂的blender表达式"""
        self.operator.add_strategy('dma')
        complex_expr = 's0*2 + s1 - max(s0, s1)'
        self.operator.set_blender(complex_expr, 'Group_1')
        self.assertEqual(self.operator.strategy_groups['Group_1'].blender_str, complex_expr)

    def test_blender_dict_partial_groups(self):
        """测试字典形式的blender只设置部分组"""
        self.operator.add_strategy('dma')
        self.operator.add_strategy('macd', run_freq='h', run_timing='open')
        self.operator.add_strategy('dma', run_freq='w', run_timing='close')

        # 只设置部分组
        partial_blender_dict = {'Group_1': 's0+s1'}
        self.operator.set_blender(partial_blender_dict, None)

        # 验证只设置了指定的组
        groups = self.operator.strategy_groups
        self.assertEqual(groups['Group_1'].blender_str, 's0+s1')
        # Group_2和Group_3的blender_str应为None或默认值

    def test_blender_empty_string(self):
        """测试空字符串 blender：写入后 getter 返回默认表达式"""
        self.operator.add_strategy('dma')
        self.operator.set_blender('', 'Group_1')
        # 空串写入后，getter 按当前 members 返回默认（1 个策略为 s0）
        self.assertEqual(self.operator.strategy_groups['Group_1'].strategy_count, 1)
        self.assertEqual(self.operator.strategy_groups['Group_1'].blender_str, 's0')


class TestStrategyTracing(unittest.TestCase):
    """测试BaseStrategy类的trace相关方法"""

    def setUp(self):
        """设置测试环境"""
        # 创建一个简单的策略实例用于测试
        self.strategy = BaseStrategy(
                name='test_strategy',
                description='Test strategy for tracing',
        )
        # 设置策略ID用于测试
        self.strategy._strategy_id = 'test_id'

    def test_disable_tracing_initially(self):
        """测试初始状态下追踪功能是禁用的"""
        self.assertFalse(self.strategy._trace_enabled)
        self.assertEqual(self.strategy._trace_max_steps, 0)
        self.assertEqual(self.strategy._trace_step, 0)
        self.assertEqual(len(self.strategy._trace_data), 0)

    def test_enable_tracing_with_valid_max_steps(self):
        """测试启用追踪功能，使用有效的max_steps"""
        max_steps = 10
        self.strategy.enable_tracing(max_steps)

        self.assertTrue(self.strategy._trace_enabled)
        self.assertEqual(self.strategy._trace_max_steps, max_steps)
        self.assertEqual(self.strategy._trace_step, 0)
        self.assertEqual(len(self.strategy._trace_data), 0)

    def test_enable_tracing_with_invalid_max_steps(self):
        """测试使用无效max_steps启用追踪功能"""
        # 测试非整数输入
        with self.assertRaises(ValueError):
            self.strategy.enable_tracing("invalid")

        # 测试负数输入
        with self.assertRaises(ValueError):
            self.strategy.enable_tracing(-5)

        # 测试零输入
        with self.assertRaises(ValueError):
            self.strategy.enable_tracing(0)

    def test_disable_tracing_method(self):
        """测试禁用追踪功能"""
        # 先启用追踪
        self.strategy.enable_tracing(5)
        self.strategy.trace(10, 'test_var')

        # 确认追踪已启用
        self.assertTrue(self.strategy._trace_enabled)
        self.assertEqual(len(self.strategy._trace_data), 1)

        # 禁用追踪
        self.strategy.disable_tracing()

        # 确认追踪已禁用
        self.assertFalse(self.strategy._trace_enabled)
        self.assertEqual(self.strategy._trace_max_steps, 0)
        self.assertEqual(self.strategy._trace_step, 0)
        self.assertEqual(len(self.strategy._trace_data), 0)

    def test_trace_with_int_variable(self):
        """测试追踪整数变量"""
        self.strategy.enable_tracing(5)

        # 追踪一个整数变量
        self.strategy.trace('int_var', 42)

        # 检查追踪数据
        trace_data = self.strategy.get_trace_data()
        print(f'get trace data: \n{trace_data}')
        self.assertIn('test_id_int_var', trace_data)
        self.assertEqual(trace_data['test_id_int_var'][0], 42)

    def test_trace_with_float_variable(self):
        """测试追踪浮点数变量"""
        self.strategy.enable_tracing(5)

        # 追踪一个浮点数变量
        self.strategy.trace('float_var', 3.14)

        # 检查追踪数据
        trace_data = self.strategy.get_trace_data()
        print(f'get trace data: \n{trace_data}')
        self.assertIn('test_id_float_var', trace_data)
        self.assertEqual(trace_data['test_id_float_var'][0], 3.14)

    def test_trace_with_boolean_variable(self):
        """测试追踪布尔变量"""
        print(f'test trace_with_boolean_variable')
        self.strategy.enable_tracing(5)

        # 追踪一个布尔变量
        self.strategy.trace('bool_var', True)

        # 检查追踪数据
        trace_data = self.strategy.get_trace_data()
        print(f'got trace data: \n{trace_data}')
        self.assertIn('test_id_bool_var', trace_data)
        self.assertEqual(trace_data['test_id_bool_var'][0], True)

    def test_trace_with_string_variable(self):
        """测试追踪字符串变量"""
        print('test_trace_with_string_variable')
        self.strategy.enable_tracing(5)

        # 追踪一个字符串变量
        self.strategy.trace("test_string", 'str_var')

        # 检查追踪数据
        trace_data = self.strategy.get_trace_data()
        print(f'got trace data: \n{trace_data}')
        self.assertIn('test_id_test_string', trace_data)
        self.assertEqual(trace_data['test_id_test_string'][0], "str_var")

    def test_trace_with_multiple_variables(self):
        """测试追踪多个变量"""
        print(f'test trace with multiple variables')
        self.strategy.enable_tracing(5)

        # 追踪多个变量
        self.strategy.trace('var1', 1)
        self.strategy.trace('var2', 2.5)
        self.strategy.trace('var3', False)

        # 检查追踪数据
        trace_data = self.strategy.get_trace_data()
        print(f'got trace data: \n{trace_data}')
        self.assertEqual(len(trace_data), 5)
        self.assertIn('test_id_var1', trace_data)
        self.assertIn('test_id_var2', trace_data)
        self.assertIn('test_id_var3', trace_data)
        self.assertEqual(trace_data['test_id_var1'][0], 1)
        self.assertEqual(trace_data['test_id_var2'][0], 2.5)
        self.assertEqual(trace_data['test_id_var3'][0], False)

    def test_trace_with_comments(self):
        """测试追踪变量时添加注释"""
        print(f'test trace with comments')
        self.strategy.enable_tracing(5)

        # 追踪一个变量并添加注释
        self.strategy.trace('commented_var', 'This is a test comment')

        # 检查追踪数据
        trace_data = self.strategy.get_trace_data()
        print(f'get trace data: \n{trace_data}')
        self.assertIn('test_id_commented_var', trace_data)
        self.assertEqual(trace_data['test_id_commented_var'][0], 'This is a test comment')

    def test_trace_with_max_steps_limit(self):
        """测试追踪变量时不超过最大步数限制"""
        print(f'test trace with max steps limit')
        max_steps = 3
        self.strategy.enable_tracing(max_steps)

        # 追踪超过最大步数限制的变量
        for i in range(max_steps + 2):  # 追踪5个值，但最大步数是3
            self.strategy.trace('limited_var', i)
            if i < max_steps:  # 只有前max_steps个值会被记录
                self.strategy._trace_step += 1  # 模拟步数前进

        # 检查追踪数据，只有前max_steps个值被记录
        trace_data = self.strategy.get_trace_data()
        print(f'get trace data: \n{trace_data}')
        if 'limited_var' in trace_data:
            # 实际记录的数量取决于trace方法如何处理超过限制的情况
            recorded_count = len(trace_data['limited_var'])
            # 根据trace方法的实现，它会在达到max_steps时停止记录
            self.assertLessEqual(recorded_count, max_steps)

    def test_get_trace_data_empty_when_disabled(self):
        """测试当追踪功能禁用时get_trace_data返回空结果"""
        # 确保追踪功能被禁用
        print(f'test get_trace_data when tracing is disabled')
        self.strategy.disable_tracing()

        # 尝试追踪一个变量（这应该不会被记录）
        self.strategy.trace('disabled_var', 123)

        # 获取追踪数据
        trace_data = self.strategy.get_trace_data()
        print(f'get trace data: \n{trace_data}')

        # 应该返回空字典
        self.assertEqual(len(trace_data), 0)

    def test_trace_dtype_handling(self):
        """测试追踪不同数据类型时的数据类型处理"""
        print('test trace dtype handling')
        self.strategy.enable_tracing(5)

        # 追踪不同类型的变量并检查内部数据类型
        self.strategy.trace('int_var', 10)  # 应该是int64
        self.strategy.trace('float_var', 3.14)  # 应该是float64
        self.strategy.trace('bool_var', True)  # 应该是bool
        self.strategy.trace('str_var', "text")  # 应该是object

        trace_data = self.strategy.get_trace_data()
        print(f'get trace data: \n{trace_data}')

        # 验证数据被正确存储
        self.assertEqual(trace_data['test_id_int_var'][0], 10)
        self.assertEqual(trace_data['test_id_float_var'][0], 3.14)
        self.assertEqual(trace_data['test_id_bool_var'][0], True)
        self.assertEqual(trace_data['test_id_str_var'][0], "text")

    def test_trace_step_tracking(self):
        """测试追踪步骤的记录"""
        self.strategy.enable_tracing(5)

        # 模拟追踪多个步骤
        self.strategy._trace_step = 0
        self.strategy.trace('step_var', 1)

        self.strategy._trace_step = 1
        self.strategy.trace('step_var', 2)

        self.strategy._trace_step = 2
        self.strategy.trace('step_var', 3)

        trace_data = self.strategy.get_trace_data()
        print(f'get trace data: \n{trace_data}')
        self.assertEqual(len(trace_data['test_id_step_var']), 5)
        self.assertEqual(trace_data['test_id_step_var'][0], 1)
        self.assertEqual(trace_data['test_id_step_var'][1], 2)
        self.assertEqual(trace_data['test_id_step_var'][2], 3)


class TestOperatorSetParameter(unittest.TestCase):
    """完整测试 Operator.set_parameter 的全部参数与边界条件，不使用 Mock。"""

    def setUp(self):
        self.op = qt.Operator()
        self.op.add_strategies('dma, macd', run_freq='d', run_timing='close')
        self.dma_id = self.op.strategy_ids[0]
        self.macd_id = self.op.strategy_ids[1]

    # ---------- stg_id 边界 ----------
    def test_set_parameter_stg_id_valid_str(self):
        self.op.set_parameter(self.dma_id, par_values=(50, 10, 20))
        self.assertEqual(self.op[self.dma_id].par_values, (50, 10, 20))

    def test_set_parameter_stg_id_valid_int(self):
        self.op.set_parameter(0, par_values=(50, 10, 20))
        self.assertEqual(self.op[0].par_values, (50, 10, 20))
        self.op.set_parameter(1, par_values=(11, 27, 8))
        self.assertEqual(self.op[1].par_values, (11, 27, 8))

    def test_set_parameter_stg_id_int_negative_raises(self):
        """stg_id 为负整数（如 -1）时应报错，不支持“最后一个策略”的语义。"""
        self.assertRaises(KeyError, self.op.set_parameter, -1, par_values=(11, 27, 8))

    def test_set_parameter_stg_id_none_raises(self):
        self.assertRaises(AssertionError, self.op.set_parameter, None, par_values=(1, 2, 3))

    def test_set_parameter_stg_id_wrong_type_raises(self):
        self.assertRaises(AssertionError, self.op.set_parameter, 1.5, par_values=(1, 2, 3))
        self.assertRaises(AssertionError, self.op.set_parameter, [], par_values=(1, 2, 3))

    def test_set_parameter_stg_id_nonexistent_str_raises(self):
        self.assertRaises(KeyError, self.op.set_parameter, 'nonexistent_id', par_values=(1, 2, 3))

    def test_set_parameter_stg_id_int_out_of_range_raises(self):
        self.assertRaises(KeyError, self.op.set_parameter, 10, par_values=(1, 2, 3))
        self.assertRaises(KeyError, self.op.set_parameter, -10, par_values=(1, 2, 3))

    # ---------- pars ----------
    def test_set_parameter_pars_single_parameter(self):
        from qteasy.parameter import Parameter
        new_par = Parameter(par_range=(5, 100), par_type='int', name='slow')
        self.op.set_parameter(self.dma_id, pars=new_par)
        self.assertEqual(self.op[self.dma_id].pars['slow'].par_range, (5, 100))

    def test_set_parameter_pars_list(self):
        from qteasy.parameter import Parameter
        pars = [
            Parameter(par_range=(5, 100), par_type='int', name='slow'),
            Parameter(par_range=(5, 100), par_type='int', name='long'),
            Parameter(par_range=(5, 100), par_type='int', name='diff'),
        ]
        self.op.set_parameter(self.dma_id, pars=pars)
        self.assertEqual(self.op[self.dma_id].pars['slow'].par_range, (5, 100))

    def test_set_parameter_pars_wrong_type_raises(self):
        self.assertRaises(TypeError, self.op.set_parameter, self.dma_id, pars='not_a_par')

    # ---------- opt_tag ----------
    def test_set_parameter_opt_tag_0_1_2(self):
        self.op.set_parameter(self.dma_id, opt_tag=0)
        self.assertEqual(self.op[self.dma_id].opt_tag, 0)
        self.op.set_parameter(self.dma_id, opt_tag=1)
        self.assertEqual(self.op[self.dma_id].opt_tag, 1)
        self.op.set_parameter(self.dma_id, opt_tag=2)
        self.assertEqual(self.op[self.dma_id].opt_tag, 2)

    # ---------- data_types ----------
    def test_set_parameter_data_types_single(self):
        self.op.set_parameter(
                self.dma_id,
                data_types=DataType('close', freq='d', asset_type='ANY'),
                window_length=100,
        )
        self.assertEqual(self.op[self.dma_id].window_lengths.get('close_ANY_d'), 100)

    def test_set_parameter_data_types_list(self):
        self.op.set_parameter(
                self.dma_id,
                data_types=[DataType('close', freq='d', asset_type='ANY')],
                window_length=80,
        )
        self.assertEqual(self.op[self.dma_id].max_window_length, 80)

    # ---------- data_type_id + window_length + use_latest_data_cycle ----------
    def test_set_parameter_data_type_id_window_length(self):
        self.op.set_parameter(
                self.dma_id,
                data_type_id='close_ANY_d',
                window_length=120,
        )
        self.assertEqual(self.op[self.dma_id].window_lengths['close_ANY_d'], 120)

    def test_set_parameter_data_type_id_use_latest_data_cycle(self):
        self.op.set_parameter(
                self.dma_id,
                data_type_id='close_ANY_d',
                use_latest_data_cycle=True,
        )
        self.assertTrue(self.op[self.dma_id].get_data_ulc('close_ANY_d'))

    def test_set_parameter_data_type_id_nonexistent_raises(self):
        self.assertRaises(
                KeyError,
                self.op.set_parameter,
                self.dma_id,
                data_type_id='wrong_dtype_id',
                window_length=10,
        )

    def test_set_parameter_window_length_scalar(self):
        self.op.set_parameter(self.dma_id, window_length=50)
        self.assertEqual(self.op[self.dma_id].window_lengths['close_ANY_d'], 50)

    def test_set_parameter_window_length_dict(self):
        self.op.set_parameter(self.dma_id, window_length={'close_ANY_d': 88})
        self.assertEqual(self.op[self.dma_id].window_lengths['close_ANY_d'], 88)

    def test_set_parameter_use_latest_data_cycle_scalar(self):
        self.op.set_parameter(self.dma_id, use_latest_data_cycle=True)
        self.assertTrue(self.op[self.dma_id].get_data_ulc('close_ANY_d'))

    def test_set_parameter_use_latest_data_cycle_dict(self):
        self.op.set_parameter(self.dma_id, use_latest_data_cycle={'close_ANY_d': False})
        self.assertFalse(self.op[self.dma_id].get_data_ulc('close_ANY_d'))

    # ---------- freq + asset_type（需多数据类型策略）----------
    def test_set_parameter_freq_asset_type_with_multi_dtype_strategy(self):
        stg = BaseStrategy(
                name='MultiDtypeStg',
                pars=[param1.copy(), param2.copy()],
                data_types=[dtype_1.copy(), dtype_2.copy(), dtype_4.copy()],
                use_latest_data_cycle=False,
                window_length=20,
        )
        self.op.add_strategy(stg, run_freq='d', run_timing='close')
        stg_id = stg.strategy_id
        self.assertIn('close_E_d', self.op[stg_id].data_types)
        self.op.set_parameter(stg_id, data_type_id='close_E_15min', freq='30min')
        self.assertIn('close_E_30min', self.op[stg_id].data_types)
        self.assertNotIn('close_E_15min', self.op[stg_id].data_types)
        self.op.set_parameter(stg_id, data_type_id='close_E_d', asset_type='IDX')
        self.assertIn('close_IDX_d', self.op[stg_id].data_types)
        self.assertNotIn('close_E_d', self.op[stg_id].data_types)

    # ---------- par_values ----------
    def test_set_parameter_par_values_tuple(self):
        self.op.set_parameter(self.dma_id, par_values=(20, 30, 10))
        self.assertEqual(self.op[self.dma_id].par_values, (20, 30, 10))

    def test_set_parameter_par_values_list(self):
        self.op.set_parameter(self.macd_id, par_values=[14, 28, 7])
        self.assertEqual(self.op[self.macd_id].par_values, (14, 28, 7))

    def test_set_parameter_par_values_kwargs_rule_iterator(self):
        """按名字设置单组参数：par_values 为 {参数名: 值} 时按 kwargs 传入。"""
        self.op.set_parameter(
                self.dma_id,
                par_values={'slow': 15, 'long': 25, 'diff': 8},
        )
        self.assertEqual(self.op[self.dma_id].par_values, (15, 25, 8))

    def test_set_parameter_par_values_multi_par_with_default(self):
        """multi_par 标准格式：{stock_id: (par_tuple), 'default': (par_tuple)}；按 share_id/default 解析后与 share_names 同序，
        len(multi_pars)==share_count 与 RuleIterator.generate() 中 for i in range(self.share_count); par=self.multi_pars[i] 一致。"""
        shares = ['000001', '000002', '000003']
        self.op.set_shares(shares)
        self.op.set_parameter(
                self.dma_id,
                par_values={
                    '000001':  (12, 25, 24),
                    '000002':  (12, 26, 27),
                    'default': (12, 24, 26),
                },
        )
        stg = self.op[self.dma_id]
        self.assertTrue(stg.allow_multi_par)
        self.assertIsNotNone(stg.multi_pars)
        self.assertEqual(len(stg.multi_pars), len(shares))
        self.assertEqual(stg.multi_pars[0], (12, 25, 24), '000001 应使用指定参数')
        self.assertEqual(stg.multi_pars[1], (12, 26, 27), '000002 应使用指定参数')
        self.assertEqual(stg.multi_pars[2], (12, 24, 26), '000003 应使用 default 参数')

    def test_set_parameter_par_values_multi_par_dict_key_order_follows_share_names(self):
        """解析顺序以 share_names 为准，与 dict 的 key 顺序无关。
        
        约定：实现必须按 share_names 顺序遍历并查 dict，得到与 share_names 同序的 multi_pars 序列。
        len(multi_pars)==share_count 与 RuleIterator.generate() 中 for i in range(self.share_count); par=self.multi_pars[i] 一致。
        """
        shares = ['000001', '000002', '000003']
        print(f'before setting multi_pars: '
              f'par_values: {self.op[self.dma_id].par_values}, multi_pars: {self.op[self.dma_id].multi_pars}')
        self.op.set_shares(shares)
        self.op.set_parameter(
                self.dma_id,
                par_values={
                    'default': (12, 24, 26),
                    '000002':  (12, 26, 27),
                    '000001':  (12, 25, 24),
                    '000003':  (12, 23, 25),
                },
        )
        stg = self.op[self.dma_id]
        print(f'par_values: {stg.par_values}, multi_pars: {stg.multi_pars}')
        self.assertEqual(len(stg.multi_pars), len(shares))
        self.assertEqual(stg.multi_pars[0], (12, 25, 24), '000001 按 share_names 顺序')
        self.assertEqual(stg.multi_pars[1], (12, 26, 27), '000002 按 share_names 顺序')
        self.assertEqual(stg.multi_pars[2], (12, 23, 25), '000003 按 share_names 顺序')
        self.assertEqual(stg.par_values, (12, 25, 24), '整体 par_values 为最新的multi_pars')

    def test_set_parameter_par_values_multi_par_wrong_par_values(self):
        """multi_par 中某股票的 par_tuple 类型错误时应报错。

        约定：每个 (par_tuple) 必须为 tuple 或 list, 且可以被正确设置为par_values
        """
        shares = ['000001', '000002']
        self.op.set_shares(shares)
        self.assertRaises(
                (ValueError, TypeError),
                self.op.set_parameter,
                self.dma_id,
                par_values={
                    '000001':  (12, 25, 24),
                    '000002':  'not_a_tuple',
                    'default': (12, 24, 26),
                },
        )
        self.assertRaises(
                KeyError,
                self.op.set_parameter,
                self.dma_id,
                par_values={
                    '000001': (12, 25, 24),
                },

        )

        self.assertRaises(
                (ValueError, TypeError),
                self.op.set_parameter,
                self.dma_id,
                par_values={
                    '000001': (12, 25, 24),
                    '000002': (12, 26),  # 长度错误
                },
        )

        self.assertRaises(
                (ValueError, TypeError),
                self.op.set_parameter,
                self.dma_id,
                par_values={
                    '000001': (12, 25, 24),
                    '000002': (12, 0, 6),  # 范围超限
                },
        )

        self.assertRaises(
                (ValueError, TypeError),
                self.op.set_parameter,
                self.dma_id,
                par_values={
                    '000001': (12, 25, 24),
                    '000002': (12, 3.14, 6),  # dtype错误
                },
        )

    def test_set_parameter_par_values_multi_par_multiple_shares_use_default(self):
        """多只 share 未在 dict 中显式出现时，均使用 default。
        
        约定：未在 dict 中出现的 share_names[i] 使用 'default' 对应的 par_tuple。
        len(multi_pars)==share_count 与 RuleIterator.generate() 中 for i in range(self.share_count); par=self.multi_pars[i] 一致。
        """
        shares = ['000001', '000002', '000003']
        self.op.set_shares(shares)
        self.op.set_parameter(
                self.dma_id,
                par_values={
                    '000001':  (12, 25, 24),
                    'default': (12, 24, 26),
                },
        )
        stg = self.op[self.dma_id]
        self.assertEqual(len(stg.multi_pars), len(shares))
        self.assertEqual(stg.multi_pars[0], (12, 25, 24), '000001 显式指定')
        self.assertEqual(stg.multi_pars[1], (12, 24, 26), '000002 使用 default')
        self.assertEqual(stg.multi_pars[2], (12, 24, 26), '000003 使用 default')

    def test_set_parameter_par_values_multi_par_share_mismatch(self):
        """multi_par 中股票代码与 share_names 不完全匹配：仅按 share_names 解析，未出现的 share_id 忽略。
        
        len(multi_pars)==share_count 与 RuleIterator.generate() 中 for i in range(self.share_count); par=self.multi_pars[i] 一致。
        """
        shares = ['000001', '000002']
        self.op.set_shares(shares)
        self.op.set_parameter(
                self.dma_id,
                par_values={
                    '000001':  (12, 25, 24),
                    '000003':  (12, 26, 27),
                    'default': (12, 24, 26),
                },
        )
        stg = self.op[self.dma_id]
        self.assertIsNotNone(stg.multi_pars)
        self.assertEqual(len(stg.multi_pars), len(shares))
        self.assertEqual(stg.multi_pars[0], (12, 25, 24), '000001 应使用指定参数')
        self.assertEqual(stg.multi_pars[1], (12, 24, 26),
                         '000002 不在 dict 中应使用 default；000003 在 dict 中但不在 share_names 中应忽略')

    def test_set_parameter_par_values_multi_par_before_set_shares_raises(self):
        """先 set_parameter(multi_par) 后 set_shares 时应报错：share_count 未定无法解析。
        
        约定 A：此时报错（例如 ValueError），提示需先 set_shares 或 share_count 未定。
        若实现改为"延后解析"（约定 B）：先接受并暂存 dict，在 set_shares 或 generate 时再按 share_names 解析，
        则本用例应改为断言：先 set_parameter(multi_par) 再 set_shares 后，len(stg.multi_pars)==share_count。
        """
        op = qt.Operator()
        op.add_strategy(DMA(), run_freq='d', run_timing='close')
        stg_id = op.strategy_ids[0]
        self.assertRaises(
                (ValueError, TypeError, KeyError),
                op.set_parameter,
                stg_id,
                par_values={'000001': (12, 5, 4), 'default': (12, 4, 6)},
        )

    def test_set_parameter_par_values_multi_par_only_default_raises(self):
        """multi_par 仅含 default 时应报错：至少有一个非 default 的 stock_id。
        
        约定：multi_par 中至少有一个非 'default' 的 stock_id。
        """
        self.op.set_shares(['000001', '000002'])
        self.assertRaises(
                ValueError,
                self.op.set_parameter,
                self.dma_id,
                par_values={'default': (12, 4, 6)},
        )

    def test_set_parameter_par_values_multi_par_value_length_mismatch_raises(self):
        """multi_par 中某 value 长度与 par_count 不一致时应报错。
        
        约定：每个 (par_tuple) 长度必须等于 strategy.par_count。
        """
        self.op.set_shares(['000001', '000002'])
        self.assertRaises(
                (ValueError, TypeError),
                self.op.set_parameter,
                self.dma_id,
                par_values={'000001': (12, 5), '000002': (12, 6, 7), 'default': (12, 4, 6)},
        )

    def test_set_parameter_par_values_multi_par_disallowed_raises(self):
        """allow_multi_par=False 时传入 multi_par 应报错。"""
        op = qt.Operator()
        stg = DMA(allow_multi_par=False)
        op.add_strategy(stg, run_freq='d', run_timing='close')
        stg_id = stg.strategy_id
        op.set_shares(['000001', '000002'])
        self.assertRaises(
                (ValueError, TypeError),
                op.set_parameter,
                stg_id,
                par_values={'000001': (12, 5, 4), '000002': (12, 6, 7), 'default': (12, 4, 6)},
        )

    def test_set_parameter_par_values_tuple_list_not_multi_par(self):
        """tuple/list 形式的 par_values 解析为普通位置参数，不作为 multi_par；multi_par 仅接受 dict。"""
        self.op.set_shares(['000001', '000002'])
        self.op.set_parameter(
                self.dma_id,
                par_values=(12, 25, 34),
        )
        stg = self.op[self.dma_id]
        self.assertIsNone(stg.multi_pars, 'tuple/list 不应被当作 multi_par 设置')

    def test_set_parameter_par_values_wrong_length_raises(self):
        self.assertRaises(
                ValueError,
                self.op.set_parameter,
                self.dma_id,
                par_values=(1, 2),
        )
        self.assertRaises(
                ValueError,
                self.op.set_parameter,
                self.dma_id,
                par_values=(1, 2, 3, 4),
        )

    # ---------- par_range ----------
    def test_set_parameter_par_range_tuple(self):
        self.op.set_parameter(
                self.dma_id,
                par_range=((15, 100), (15, 100), (5, 80)),
        )
        self.assertEqual(self.op[self.dma_id].pars['slow'].par_range, (15, 100))
        self.assertEqual(self.op[self.dma_id].pars['diff'].par_range, (5, 80))

    def test_set_parameter_par_range_dict(self):
        self.op.set_parameter(
                self.dma_id,
                par_range={'slow': (20, 150), 'long': (20, 150), 'diff': (8, 120)},
        )
        self.assertEqual(self.op[self.dma_id].pars['slow'].par_range, (20, 150))

    def test_set_parameter_par_range_wrong_type_raises(self):
        self.assertRaises(
                TypeError,
                self.op.set_parameter,
                self.dma_id,
                par_range='not_list_or_dict',
        )

    def test_set_parameter_par_range_wrong_length_raises(self):
        self.assertRaises(
                ValueError,
                self.op.set_parameter,
                self.dma_id,
                par_range=((10, 250), (10, 250)),
        )

    # ---------- run_freq / run_timing ----------
    def test_set_parameter_run_freq_same_value_no_move(self):
        g0 = self.op[self.dma_id]._group_id
        self.op.set_parameter(self.dma_id, run_freq='d')
        self.assertEqual(self.op[self.dma_id]._group_id, g0)
        self.assertEqual(self.op[self.dma_id].run_freq, 'd')

    def test_set_parameter_run_freq_change_strategy_alone_in_group(self):
        self.assertEqual(len(self.op.groups), 1)
        self.assertEqual(len(self.op.groups['Group_1'].members), 2)
        op2 = qt.Operator()
        op2.add_strategy(DMA(), run_freq='d', run_timing='close')
        self.assertEqual(len(op2.groups['Group_1'].members), 1)
        op2.set_parameter(op2.strategy_ids[0], run_freq='h')
        self.assertEqual(op2[str(op2.strategy_ids[0])].run_freq, 'h')
        self.assertEqual(op2.groups['Group_1'].run_freq, 'h')

    def test_set_parameter_run_freq_change_strategy_not_alone(self):
        self.op.set_parameter(self.dma_id, run_freq='h', run_timing='close')
        self.assertEqual(self.op[self.dma_id].run_freq, 'h')
        self.assertEqual(self.op[self.macd_id].run_freq, 'd')
        self.assertEqual(len(self.op.groups), 2)
        g_d = [g for g in self.op.groups.values() if g.run_freq == 'd'][0]
        g_h = [g for g in self.op.groups.values() if g.run_freq == 'h'][0]
        self.assertEqual(len(g_d.members), 1)
        self.assertEqual(len(g_h.members), 1)

    def test_set_parameter_run_timing_change(self):
        self.op.set_parameter(self.dma_id, run_timing='open')
        self.assertEqual(self.op[self.dma_id].run_timing, 'open')
        self.assertEqual(self.op[self.macd_id].run_timing, 'close')

    def test_set_parameter_run_freq_change_merges_two_groups(self):
        """两 Group 各一个 Strategy，修改其中一个的 run_freq/run_timing 与另一 Group 相同，应合并为一 Group。"""
        op = qt.Operator()
        op.add_strategy(DMA(), run_freq='d', run_timing='close')
        op.add_strategy(MACD(), run_freq='h', run_timing='close')
        self.assertEqual(len(op.groups), 2)
        self.assertEqual(len(op.groups['Group_1'].members), 1)
        self.assertEqual(len(op.groups['Group_2'].members), 1)
        dma_id = op.strategy_ids[0]
        macd_id = op.strategy_ids[1]
        self.assertEqual(op[dma_id].run_freq, 'd')
        self.assertEqual(op[macd_id].run_freq, 'h')
        op.set_parameter(macd_id, run_freq='d', run_timing='close')
        self.assertEqual(len(op.groups), 1)
        only_group = list(op.groups.values())[0]
        self.assertEqual(only_group.strategy_count, 2)
        self.assertEqual(only_group.run_freq, 'd')
        self.assertEqual(only_group.run_timing, 'close')
        self.assertEqual(op[dma_id]._group_id, op[macd_id]._group_id)
        self.assertEqual(op[dma_id].run_freq, 'd')
        self.assertEqual(op[macd_id].run_freq, 'd')
        self.assertEqual(op[dma_id].run_timing, 'close')
        self.assertEqual(op[macd_id].run_timing, 'close')

    # ---------- kwargs -> set_custom_pars ----------
    def test_set_parameter_custom_pars_valid_attr(self):
        self.op[self.dma_id]._test_attr = 0
        self.op.set_parameter(self.dma_id, _test_attr=99)
        self.assertEqual(self.op[self.dma_id]._test_attr, 99)

    def test_set_parameter_custom_pars_invalid_attr_raises(self):
        self.assertRaises(
                KeyError,
                self.op.set_parameter,
                self.dma_id,
                nonexistent_attr=1,
        )

    # ---------- 组合多参数 ----------
    def test_set_parameter_combined_multiple_params(self):
        self.op.set_parameter(
                self.dma_id,
                opt_tag=1,
                par_values=(18, 28, 9),
                window_length=100,
                use_latest_data_cycle=True,
        )
        self.assertEqual(self.op[self.dma_id].opt_tag, 1)
        self.assertEqual(self.op[self.dma_id].par_values, (18, 28, 9))
        self.assertEqual(self.op[self.dma_id].window_lengths['close_ANY_d'], 100)
        self.assertTrue(self.op[self.dma_id].get_data_ulc('close_ANY_d'))

    def test_set_parameter_combined_data_types_and_update(self):
        self.op.set_parameter(
                self.dma_id,
                data_types=DataType('close', freq='d', asset_type='ANY'),
                window_length=90,
                use_latest_data_cycle=False,
        )
        self.op.set_parameter(
                self.dma_id,
                data_type_id='close_ANY_d',
                window_length=95,
        )
        self.assertEqual(self.op[self.dma_id].window_lengths['close_ANY_d'], 95)


if __name__ == '__main__':
    unittest.main()