# -*- encoding:utf-8 -*-
"""
    技术线内部执行模块
"""
import math
from enum import Enum
import numpy as np
import pandas as pd
from scipy import optimize as sco, stats
from sklearn.cluster import KMeans
from sklearn.linear_model import LinearRegression
from ultron.utilities.logger import kd_logger
from ultron.ump.core.fixes import xrange
from ultron.kdutils.progress import Progress
from ultron.kdutils import regression
from ultron.ump.core.helper import pd_resample
from ultron.ump.similar.corrcoef import ECoreCorrType, corr_xy

pd.options.display.precision = 4
# numpy精度浮点数显示4位，不使用科学计数法
np.set_printoptions(precision=4, suppress=True)


class ESkeletonHow(Enum):
    """计算骨架走势使用的how"""
    """使用最小值取骨架点位"""
    skeleton_min = 0
    """使用最大值取骨架点位"""
    skeleton_max = 1
    """使用平均值取骨架点位"""
    skeleton_mean = 2
    """使用中位数取骨架点位"""
    skeleton_median = 3
    """使用时间序列最后的元素取骨架点位"""
    skeleton_close = 4
    """
        使用三角模式，即最高，最低，第三点：
        确定取最大值，最小值，第三个点位how_func提供，
        如果np.argmax(arr) > np.argmin(arr)即最大值位置在最小值前面，
        第三点取序列起点，否则取序列终点
    """
    skeleton_triangle = 100


class EShiftDistanceHow(Enum):
    """计算位移路程比的how"""
    """
        使用时间序列最后的元素做为路程的计算基础：
        对应序列的最后一个点位值，标准路程点位值定义
    """
    shift_distance_close = 0
    """
        使用极限值做为路程的计算基础：
        如果p_arr[0] > p_arr[-1]，使用np.min(p_arr)，否则np.max(p_arr)，即上升趋势取max，下跌趋势取min
    """
    shift_distance_maxmin = 1
    """
        使用序列的sum和极限值为路程的计算基础：
        如果abs(p_arr.max() - p_arr[-1]) > abs(p_arr[-1] - p_arr.min()) 取np.min(p_arr)否则np.max(p_arr)
    """
    shift_distance_sum_maxmin = 2


def bfgs_min_pos(find_min_pos, y_len, linear_interp):
    """
    通过scipy.interpolate.interp1d插值形成的模型，通过sco.fmin_bfgs计算min
    :param find_min_pos: 寻找min的点位值
    :param y_len: 原始序列长度，int
    :param linear_interp: scipy.interpolate.interp1d插值形成的模型
    :return: sco.fmin_bfgs成功找到的值，所有失败的或者异常都返回－1
    """
    try:
        local_min_pos = sco.fmin_bfgs(linear_interp, find_min_pos,
                                      disp=False)[0]
    except:
        # 所有失败的或者异常都返回－1
        local_min_pos = -1
    if local_min_pos < 0 or local_min_pos > y_len:
        # 所有失败的或者异常都返回－1
        local_min_pos = -1
    return local_min_pos


def calc_kl_speed(kl, resample=5):
    """
    计算曲线跟随趋势的速度，由于速度是相对的，所以需要在相同周期内与参数曲线进行比对
    :param kl: pd.Series或者numpy序列
    :param resample: 计算速度数值的重采样周期默认5
    :return: 趋势变化敏感速度
    """
    kl_rp = pd_resample(kl, '{}D'.format(resample), how='mean')
    """
        eg: kl_rp
        2011-07-28    1.0027
        2011-08-02    0.9767
        2011-08-07    0.9189
        2011-08-12    0.9174
        2011-08-17    0.9156
    """
    kl_diff = kl_rp.diff()
    """
        eg:
        2011-08-02   -0.0260
        2011-08-07   -0.0578
        2011-08-12   -0.0015
        2011-08-17   -0.0018
    """
    # 二值化 1 -1, 做为速度参数序列慢线
    # noinspection PyTypeChecker
    kl_trend_slow = pd.Series(np.where(kl_diff > 0, 1, -1))
    """
        eg: kl_trend_fast
            0     -1
            1     -1
            2     -1
            3     -1
            4     -1
            5      1
            6      1
            7     -1
            8     -1
            9     -1
    """
    # 慢线向前错一个周期形成快线
    kl_trend_fast = kl_trend_slow.shift(-1)
    """
        egL kl_trend_slow
            0     -1.0
            1     -1.0
            2     -1.0
            3     -1.0
            4      1.0
            5      1.0
            6     -1.0
            7     -1.0
            8     -1.0
            9     -1.0
    """
    kl_trend_ffs = kl_trend_fast[:-1] * kl_trend_slow[:-1]
    """
        慢线 乘 快线 即符号运算
        eg：kl_trend_ffs
        0      1.0
        1      1.0
        2      1.0
        3      1.0
        4     -1.0
        5      1.0
        6     -1.0
        7      1.0
        8      1.0
        9      1.0
    """

    # 符号相同的占所有和的比例即为趋势变化敏感速度值，注意如果没有在相同周期内与参数曲线进行比对的曲线，本速度值即无意义
    speed = kl_trend_ffs.value_counts()[1] / kl_trend_ffs.value_counts().sum()
    return speed


def shift_distance_how(how):
    """
    通过how（EShiftDistanceHow），对应出计算算路程终点点位值使用的计算方法
    注意默认使用shift_distance_close对应标准路程点位值定义方法，其它方法对应的
    路程终点点位值使用的计算方法并非得到最准确的'路程位移比'
    :param how: EShiftDistanceHow对象或者callable即外部可自行设置方法，即计算算路程终点点位值使用的计算方法可自定义
    :return: 计算算路程终点点位值使用的计算方法
    """
    if callable(how):
        # 外部可自行设置方法，即计算算路程终点点位值使用的计算方法可自定义
        return how

    if how == EShiftDistanceHow.shift_distance_maxmin:
        # 如果p_arr[0] > p_arr[-1]，使用np.min(p_arr)，否则np.max(p_arr)，即上升趋势取max，下跌趋势取min
        how = lambda p_arr: np.min(p_arr) if p_arr[0] > p_arr[-1] else np.max(
            p_arr)
    elif how == EShiftDistanceHow.shift_distance_close:
        # 对应序列的最后一个点位值，标准路程点位值定义
        how = lambda p_arr: p_arr[-1]
    elif how == EShiftDistanceHow.shift_distance_sum_maxmin:
        # 如果abs(p_arr.max() - p_arr[-1]) > abs(p_arr[-1] - p_arr.min()) 取np.min(p_arr)否则np.max(p_arr)
        # 即最终的点位绝对距离靠近np.min(p_arr)取np.min(p_arr)否则np.max(p_arr)
        how = lambda p_arr: np.min(p_arr) if abs(p_arr.max() - p_arr[-1]) > abs(p_arr[-1] - p_arr.min()) \
            else np.max(p_arr)
    else:
        raise TypeError('how is error how={}'.format(how))
    return how


def shift_distance(arr, how, slice_start=0, slice_end=-1):
    """
    计算序列arr的'位移路程比'，可视化形式为直接三角展示位移，路程直接的关系，返回
    h_distance(三角底边距离), v_distance(三角垂直距离),
    distance(斜边，路程), shift(位移), sd（位移路程比：shift / distance）
    :param arr: numpy array
    :param how: EShiftDistanceHow，计算路程终点点位值使用的计算方法
    :param slice_start: 如果arr是子序列切片，切片start值， int
    :param slice_end: 如果arr是子序列切片，切片end值， int
    :param color: 直角三角边框颜色，str对象 eg：'r', 'red'
    :return: h_distance(三角底边距离), v_distance(三角垂直距离),
            distance(斜边，路程), shift(位移), sd（位移路程比：shift / distance）
    """
    if slice_end == -1:
        # 因为下面还有slice_end - slice_start等操作，所以转换为正切片值
        slice_end = len(arr)
    # 根据slice_start， slice_end做切片
    slice_arr = arr[slice_start:slice_end]

    # 位置0的值即为路程起点点位值
    start_y = slice_arr[0]
    how = shift_distance_how(how)
    # 根据how的方法计算slice_arr出路程终点点位值
    shift_y = how(slice_arr)
    # 直角三角底边distance
    h_distance = slice_end - slice_start
    # 直角三角垂直边distance
    v_distance = abs(shift_y - start_y)
    # 计算斜边值，即路程值
    distance = math.sqrt(h_distance**2 + v_distance**2)

    # 开始计算位移，首先套上pd.Series做diff结果如下eg形式所示
    diff_slice = pd.Series(slice_arr).diff().fillna(value=0)
    """
        eg: diff_slice
            0     0.0000
            1    -3.7984
            2     7.6824
            3     9.0512
            4     4.3459
            5    -7.7679
                   ...
            44   -3.1482
            45   -1.6426
            46   -2.1216
            47    0.4449
            48   -4.6539
            49    7.1520
    """
    # np.abs(diff_slice).sum计算结果即为位移值
    shift = np.abs(diff_slice).sum()
    # 计算出位移路程比sd
    sd = shift / distance

    # h_distance(三角底边距离), v_distance(三角垂直距离),distance(斜边路程),shift(位移),sd（位移路程比：shift / distance）
    return h_distance, v_distance, distance, shift, sd


def calc_pair_speed(kl,
                    benchmark_kl,
                    kl_close=None,
                    benchmark_close=None,
                    resample=5):
    # 通过calc_kl_speed计算趋势跟随速度
    kl_speed = calc_kl_speed(kl, resample)
    benchmark_kl_speed = calc_kl_speed(benchmark_kl, resample)
    # 两个走势的SPERM相关性
    kl_close = kl if kl_close is None else kl_close
    benchmark_close = benchmark_kl if benchmark_close is None else benchmark_close
    corr = corr_xy(kl_close, benchmark_close, ECoreCorrType.E_CORE_TYPE_SPERM)
    # 返回参数symbol, benchmark_symbol所对应的趋势变化敏感速度数值, 以及相关性＊敏感度＝敏感度置信度
    return kl_speed, benchmark_kl_speed, corr


def support_resistance_pos(x, support_resistance_y, best_poly=0, label=None):
    """
    分析获取序列阻力位或者支撑位，通过sco.fmin_bfgs寻找阻力位支撑位，阻力位点也是通过sco.fmin_bfgs寻找，
    但是要求传递进来的序列已经是标准化后取反的序列
    eg：
        demean_y = ABuStatsUtil.demean(self.tl)： 首先通过demean将序列去均值
        resistance_y = demean_y * -1 ：阻力位序列要取反
        support_y = demean_y ：支持位序列不需要取反
    sco.fmin_bfgs使用的模型函数为polynomial.Chebyshev多项拟合函数，poly的次数确定
    由ABuRegUtil.search_best_poly得到，即best_poly次多项式拟合回归的趋势曲线可以比较完美的代表原始曲线y的走势，
    为了得到更多的阻力支持位种子点位值，使用：
        np.polynomial.Chebyshev.fit(x, support_resistance_y, best_poly * g_upport_resistance_unit)
        g_upport_resistance_unit默认＝3
    best_poly * 3，即将poly次数又扩大了3倍，可以改变g_upport_resistance_unit获取更多的阻力位支撑位种子点，但
    速度会更慢。
    :param x: 待分析的序列np.array
    :param support_resistance_y:
    :param best_poly: 函数使用者可设置best_poly, 设置后就不使用ABuRegUtil.search_best_poly寻找了
    :param label: 进度条显示的等待文字，str对象
    :return: 阻力位或者支撑位序列
    """
    if best_poly <= 0:
        # 由ABuRegUtil.search_best_poly得到，即best_poly次多项式拟合回归的趋势曲线可以比较完美的代表原始曲线y的走势
        # 注意这里poly_min＝7，即从7次ploy开始，提升效率
        best_poly = regression.search_best_poly(support_resistance_y,
                                                poly_min=7,
                                                zoom=False,
                                                only_poly=True)
        # 根据曲线的长度倍乘best_poly
        best_poly *= int(math.ceil(len(x) / 120))
    best_poly = int(len(x) / 20) if best_poly < int(len(x) / 20) else best_poly

    # 为了得到更多的阻力支持位种子点位值->best_poly * 3，即将poly次数又扩大了3倍
    p = np.polynomial.Chebyshev.fit(x, support_resistance_y, best_poly)

    # 需要使用set，因为需要过滤重复的
    support_resistance = set()
    # 属于耗时操作，构建进度条显示
    with Progress(len(support_resistance_y), 0, label) as progess:
        for index in xrange(0, len(support_resistance_y), 1):
            progess.show(index + 1)
            local_min_pos = int(
                bfgs_min_pos(index, len(support_resistance_y), p))
            if local_min_pos == -1:
                # 其实主要就是利用这里找不到的情况进行过滤
                continue
            # 将local_min_pos加到集合中
            support_resistance.add(local_min_pos)
    support_resistance = list(support_resistance)
    return support_resistance


def select_k_support_resistance(support_resistance, thresh=0.06):
    """
    对阻力位或者支撑位序列从1-序列个数开始聚类，多个聚类器的方差值进行比较，
    通过方差阀值等方法找到最佳聚类个数，最终得到kmean最佳分类器对象
    :param support_resistance: 阻力位或者支撑位序列
    :param thresh: 聚类方差比例阀值，默认0.06
    :param label: 可视化显示的label，主要用来区别阻力位和支撑位的聚类分析结果
    :return: 最佳分类器对象，KMeans类型
    """
    # 阻力位或者支撑位序列从1-序列个数开始聚类
    k_rng = xrange(1, len(support_resistance))
    # 聚类里某一个类别的最低点只有一个是有效
    est_arr = [KMeans(n_clusters=k).fit(support_resistance) for k in k_rng]
    sum_squares = [e.inertia_ for e in est_arr]
    """
        eg: sum_squares 形如
        [568824.24742527946, 63692.462671013389, 23023.512755456246, 11106.460471044047, 4739.3661037023803, 2
        479.206823270833, 1640.5275739375002, 1095.9099614375, 589.28019737500006, 160.28686404166669,
        45.229672916666679, 31.689656250000013, 18.94814375000001, 10.944315625000012, 5.5643156249999999,
        0.6365031249999995]
    """
    # 各类别相对中心点比， 中心点位1类
    diff_squares = [squares / sum_squares[0] for squares in sum_squares]
    diff_squares_pd = pd.Series(diff_squares)

    thresh_pd = diff_squares_pd[diff_squares_pd < thresh]
    if thresh_pd.shape[0] > 0:
        """
            eg：thresh_pd 形如index[0]代表最小的聚类个数值
            2     4.0476e-02
            3     1.9525e-02
            4     8.3319e-03
            5     4.3585e-03
            6     2.8841e-03
            7     1.9266e-03
            8     1.0360e-03
            9     2.8179e-04
        """
        select_k = k_rng[thresh_pd.index[0]]
    else:
        # 没有符合的，就安装最多的个数聚类
        select_k = k_rng[-1]

    # 通过select_k取代分类器
    est = est_arr[select_k - 1]
    return est


def support_resistance_predict(x, y, est, support_resistance, is_support):
    """
    通过最优聚类器est从阻力位或者支撑位序列support_resistance中的序列进行聚类predict
    对聚类结果进行可视化，最终从每个cluster中找到唯一值代表这个分类，如果是支撑位要找到最小值的index
    如果是阻力位找到最大值的index
    :param x: 可迭代序列x
    :param y: 可迭代序列y
    :param est: 最佳分类器对象，KMeans类型
    :param support_resistance: 阻力位或者支撑位序列
    :param is_support: 是否是进行支撑位support_resistance_predict
    :return: 返回从每个cluster中找到唯一值代表这个分类的x值组成的序列。list
    """
    support_resistance_k = est.predict(support_resistance)
    """
        eg: support_resistance_k形如：
        array([0, 0, 1, 1, 0, 1, 1, 0, 0, 2, 1, 0, 0, 2, 0, 0, 0], dtype=int32)
    """
    d_pd = pd.DataFrame(support_resistance, columns=['x', 'y'])
    d_pd['cluster'] = support_resistance_k
    """
        eg：d_pd形如：
                x        y  cluster
        0     0.0  15.5400        0
        1    97.0  21.3475        0
        2   482.0  22.9625        1
        3   387.0  21.6725        1
        4     5.0  16.2350        0
        5   426.0  23.7650        1
        6   459.0  23.2800        1
        7    48.0  13.1475        0
        8    49.0  13.6700        0
        9   283.0  20.9150        2
        10  497.0  25.2240        1
        11   53.0  13.6537        0
        12   23.0  15.3150        0
        13  280.0  19.9900        2
        14  121.0  18.3475        0
        15   27.0  15.2275        0
        16   95.0  23.9475        0
    """
    k_list = list()

    # 最终从每个cluster中找到唯一值代表这个分类
    for k in set(support_resistance_k):
        if is_support:
            # 如果是支撑位要找到最小值的index
            #ind = d_pd[d_pd['cluster'] == k]['y'].argmin()  # argmin 函数升级
            cluster_pd = d_pd[d_pd['cluster'] == k].copy()
            ind = cluster_pd['y'].argmin()
            choice_x = int(cluster_pd.iloc[ind]['x'])
        else:
            # 如果是阻力位找到最大值的index
            #ind = d_pd[d_pd['cluster'] == k]['y'].argmax()
            cluster_pd = d_pd[d_pd['cluster'] == k].copy()
            ind = cluster_pd['y'].argmax()
            choice_x = int(cluster_pd.iloc[ind]['x'])
        # 最终的结果只要x的值
        #choice_x = int(d_pd.iloc[ind]['x'])
        k_list.append(choice_x)
    # 返回从每个cluster中找到唯一值代表这个分类的x值组成的序列
    return k_list


def create_support_resistance_trend(x, y, trend_pos, label, only_last=False):
    """
    通过trend_pos绘制阻力线或者支撑线，only_last控制只绘制时间序列中最后一个发现的阻力或支撑，
    plot_org控制是否绘制线段还是直线，plot_org＝True时绘制线段，否则通过LinearRegression进行
    直线绘制
    :param x: 可迭代序列x
    :param y: 可迭代序列y
    :param trend_pos: 趋势点x序列，绘制时需要通过原始序列y获取y[x]值
    :param label: 可视化显示的文字，用来区分阻力位和支撑位
    :param only_last: 控制只绘制时间序列中最后一个发现的阻力或支撑
    """
    # trend_pos类型为list容器
    trend_pos.sort()
    if len(trend_pos) < 2:
        kd_logger.info('{} len(trend_pos) < 2 !'.format(label))
        return

    if only_last:
        # 只绘制时间序列中最后一个发现的阻力或支撑，因为前面trend_pos.sort()了，即序列中最后两个元素
        trend_pos = trend_pos[-2:]

    y_trend_arr = []
    for ind, trend_start in enumerate(trend_pos):
        if ind == len(trend_pos) - 1:
            continue
        trend_end = trend_pos[ind + 1]

        x_org = [trend_start, trend_end]
        y_org = [y[trend_start], y[trend_end]]

        # 通过LinearRegression学习线段
        reg = LinearRegression()
        reg.fit(np.array(x_org).reshape(-1, 1), np.array(y_org).reshape(-1, 1))

        x_line = [x[0], x[-1]]
        # predict序列的第一个和最后一个点，即将线段变成了直线，延伸了阻力位，支撑位
        y_line = reg.predict(np.array(x_line).reshape(-1, 1)).reshape(-1, )
        # 把端点结果返回给外面
        y_trend_arr.append(y_line)
    return y_trend_arr


def regress_trend_channel(arr):
    """
    通过arr计算拟合曲线及上下拟合通道曲线，返回三条拟合曲线，组成拟合通道
    :param arr: numpy array
    :return: y_below, y_fit, y_above
    """
    # 通过ABuRegUtil.regress_y计算拟合曲线和模型reg_mode，不使用缩放参数zoom
    reg_mode, y_fit = regression.regress_y(arr, zoom=False)
    reg_params = reg_mode.params

    x = np.arange(0, len(arr))
    a = reg_params[0]
    b = reg_params[1]
    # 通过argmin寻找出原始序列和拟合序列差值的最小点，差值最小代表点位离拟合曲线远，eg: 100 - 80 < 100 - 90
    min_ind = (arr.T - y_fit).argmin()
    # 根据a, b计算出below值, 注意这里是差，eg: below：100 － 80 ＝ 20
    below = x[min_ind] * b + a - arr[min_ind]
    # 计算x * b + a但- below，即拟合曲线保持相同的斜率整体下移below值
    y_below = x * b + a - below

    # 通过argmax寻找出原始序列和拟合序列差值的最大点，差值最小代表点位离拟合曲线远，eg: 120 - 100 > 120 - 110
    max_ind = (arr.T - y_fit).argmax()
    # 根据a, b计算出above值, 注意这里是差，eg: above 100 - 120 ＝ -20, 即above是负数
    above = x[max_ind] * b + a - arr[max_ind]
    # 计算x * b + a但整天- above，由于above是负数，即相加 即拟合曲线保持相同的斜率整体上移above值
    y_above = x * b + a - above
    return y_below, y_fit, y_above


def find_percent_point(percents, y):
    """
    技术线比例分割的区域, 针对输入的比例迭代操作后
    分别使用stats.scoreatpercentile和 (y.max() - y.min()) * pt + y.min()两种
    方式进行计算的分割值, 返回对象为比例值为key的字典对象
    eg:
        input:
            percents = (0.1, 0.9)
        output:
            {0.1: (15.732749999999999, 15.5075), 0.9: (31.995000000000005, 34.387500000000003)}

    :param percents: 可迭代序列，eg: (0.1, 0.9), [0.3, 0,4, 0.8]
    :param y: 计算分割线的序列
    :return: 比例值为key的字典对象
    """
    percent_point_dict = {
        pt: (stats.scoreatpercentile(y, np.round(pt * 100, 1)),
             (y.max() - y.min()) * pt + y.min())
        for pt in percents
    }

    return percent_point_dict


def find_golden_point_ex(x, y):
    sp382 = stats.scoreatpercentile(y, 38.2)
    sp618 = stats.scoreatpercentile(y, 61.8)
    sp50 = stats.scoreatpercentile(y, 50.0)
    return sp382, sp50, sp618


def find_golden_point(x, y):
    """视觉黄金分割计算方法"""

    cs_max = y.max()
    cs_min = y.min()

    sp382 = (cs_max - cs_min) * 0.382 + cs_min
    sp618 = (cs_max - cs_min) * 0.618 + cs_min
    sp50 = (cs_max - cs_min) * 0.5 + cs_min
    return sp382, sp618, sp50


def below_above_gen(x, y):
    """
    (y, x) if x > y else (x, y)
    :param x: 支持比较操作的对象
    :param y: 支持比较操作的对象
    """
    return (y, x) if x > y else (x, y)


def skeleton_how(how):
    """
    根据how映射计算数据序列骨架点位的方法
    :param how: ESkeletonHow对象或者callable即外部可自行设置方法，即计算数据序列骨架点位的方法可自定义
    :return:
    """
    if callable(how):
        # callable即外部可自行设置方法
        return how

    if how == ESkeletonHow.skeleton_min:
        how_func = np.min
    elif how == ESkeletonHow.skeleton_max:
        how_func = np.max
    elif how == ESkeletonHow.skeleton_mean:
        how_func = np.mean
    elif how == ESkeletonHow.skeleton_median:
        how_func = np.median
    elif how == ESkeletonHow.skeleton_close:
        # 取序列最后一个元素做为采样骨架点位
        how_func = lambda arr: arr[-1]
    elif how == ESkeletonHow.skeleton_triangle:
        # 三角模式骨架点位：确定取最大值，最小值，第三个点位how_func提供
        # 如果np.argmax(arr) > np.argmin(arr)即最大值位置在最小值前面，第三点取序列起点，否则取序列终点
        how_func = lambda arr, start: (start, arr[0]) if np.argmax(arr) > np.argmin(arr) else \
            (len(arr) + start, arr[-1])
    else:
        raise TypeError('how is error how={}'.format(how))
    return how_func