################################## 回测模块 ##################################
def analyzer(port_pnl, market_pnl, strategy, risk_free_rate=0.03, fig_name=[]):
    
    import pyfolio as pf
    import seaborn as sns
    import statsmodels.api as sm

    ########## 画图设置 ##########
    import matplotlib.pyplot as plt  
    import matplotlib
    import platform
    import pandas as pd
    import numpy as np
    import warnings
    warnings.filterwarnings("ignore")

    system = platform.system()

    # windows系统
    if system == "Windows":
        plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # windows系统
    elif system == "Darwin":  # Darwin 是 macOS 的系统标识
        # mac系统
        plt.rcParams['font.sans-serif'] = ['PingFang HK'] # mac系统
    else:
        plt.rcParams['font.sans-serif'] = ['DejaVu Sans']     # Linux 默认字体

    matplotlib.use('Qt5Agg')
    plt.switch_backend('Qt5Agg')    

    """
    回测模块
    输入：
        port_pnl: 策略收益序列
        market_pnl: 基准收益序列
        strategy: 策略名称
        risk_free_rate: 无风险利率
        fig_name: 图表名称
    输出：
        stats_all: 回测结果
    """

    from matplotlib.patches import FancyBboxPatch
    from matplotlib.ticker import FuncFormatter

    def normalize_cumulative(pnl_series):
        """
        将收益序列转换为起点为1的累计净值序列。
        """
        cumulative_returns = (1 + pnl_series).cumprod()
        if cumulative_returns.empty:
            return cumulative_returns
        return cumulative_returns / cumulative_returns.iloc[0]

    def format_period_text(start_time, end_time, multiline=False):
        """
        格式化回撤时段。
        """
        if pd.isna(start_time) or pd.isna(end_time):
            return '-'

        separator = '\n至 ' if multiline else ' 至 '
        return (
            f"{start_time.strftime('%Y-%m-%d')}"
            f"{separator}"
            f"{end_time.strftime('%Y-%m-%d')}"
        )

    def get_drawdown_details(cumulative_returns):
        """
        根据累计净值序列，计算回撤序列和最大回撤时段（峰值到谷值）。
        """
        max_return_series = cumulative_returns.cummax()
        drawdown_series = cumulative_returns / max_return_series - 1

        if drawdown_series.empty:
            return drawdown_series, pd.NaT, pd.NaT, '-'

        drawdown_end = drawdown_series.idxmin()
        drawdown_start = cumulative_returns.loc[:drawdown_end].idxmax()
        drawdown_period = format_period_text(drawdown_start, drawdown_end)

        return drawdown_series, drawdown_start, drawdown_end, drawdown_period

    def get_yearly_performance(pnl_series):
        """
        统计回测区间内每一年的收益、最大回撤和最大回撤时段。
        """
        yearly_stats = []

        for year, year_pnl in pnl_series.groupby(pnl_series.index.year):
            year_cumulative = normalize_cumulative(year_pnl)
            year_drawdown, year_dd_start, year_dd_end, year_dd_period = get_drawdown_details(year_cumulative)
            yearly_stats.append({
                '年份': str(year),
                '收益': (1 + year_pnl).prod() - 1,
                '最大回撤': abs(year_drawdown.min()) if not year_drawdown.empty else np.nan,
                '最大回撤时段': year_dd_period,
            })

        if len(yearly_stats) == 0:
            return pd.DataFrame(columns=['收益', '最大回撤', '最大回撤时段'])

        return pd.DataFrame(yearly_stats).set_index('年份')

    def get_monthly_returns_matrix(pnl_series):
        """
        生成回测期内按年-月展开的月度收益矩阵。
        """
        monthly_returns = (
            pnl_series
            .groupby([pnl_series.index.year, pnl_series.index.month])
            .apply(lambda x: (1 + x).prod() - 1)
            .unstack()
        )
        monthly_returns = monthly_returns.reindex(columns=range(1, 13))
        monthly_returns.index = monthly_returns.index.astype(str)
        monthly_returns.columns = [f'{month}月' for month in range(1, 13)]
        return monthly_returns

    def add_panel_background(ax, facecolor='white', edgecolor='#dbe3ef'):
        """
        为子图添加圆角背景，提升版式观感。
        """
        panel = FancyBboxPatch(
            (0, 0), 1, 1,
            boxstyle='round,pad=0.012,rounding_size=0.03',
            transform=ax.transAxes,
            facecolor=facecolor,
            edgecolor=edgecolor,
            linewidth=1.1,
            clip_on=False,
            zorder=-1,
        )
        ax.add_patch(panel)

    def format_stat_value(index, value):
        """
        统一格式化指标数值。
        """
        if pd.isna(value):
            return "NaN"
        if isinstance(value, str):
            return value
        if index in percentage_fields:
            return f"{value * 100:.2f}%"
        if index in integer_fields:
            return f"{int(value)}"
        return f"{value:.3f}"
    
    stats_index = ['策略收益', '策略年化收益', '超额收益','基准收益',
                   '阿尔法','贝塔','夏普比率','胜率','盈亏比','最大回撤','最大回撤时段','索提诺比率',
                   '日均超额收益','超额收益最大回撤','超额收益夏普比率','日胜率',
                   '盈利次数','亏损次数','信息比率','策略波动率','基准波动率']
    
    stats_all = pd.Series(index = stats_index, dtype='object')
    
    # 用pyfolio包统计时间段的收益指标，包括累计收益、年化收益、夏普比率、最大回撤、索提诺比率
    perf_stats = pf.timeseries.perf_stats((port_pnl)).to_frame(name='all')
    perf_stats_market = pf.timeseries.perf_stats((market_pnl)).to_frame(name='all')

    # 回撤序列
    cumulative = normalize_cumulative(port_pnl)
    drawdown, max_drawdown_start, max_drawdown_end, max_drawdown_period = get_drawdown_details(cumulative)
    cumulative_market = normalize_cumulative(market_pnl)
    
    # beta
    X = sm.add_constant(market_pnl)  # 自变量：基准收益
    y = port_pnl                     # 因变量：策略收益
    model = sm.OLS(y, X).fit()
    beta = model.params.iloc[1]  # 斜率即beta  
            
    # alpha
    annualized_strategy_return = perf_stats.loc['Annual return'][0]
    annualized_market_return = perf_stats_market.loc['Annual return'][0]
    alpha = annualized_strategy_return - \
        (risk_free_rate + beta * (annualized_market_return - risk_free_rate))

    # 策略波动率
    port_volatility = np.sqrt(250*port_pnl.var(ddof=1))
    # 基准波动率
    market_volatility = np.sqrt(250*market_pnl.var(ddof=1))
    
    # 超额收益  
    daily_ratio_alpha = (1 + port_pnl) / (1 + market_pnl) - 1   
    # 日均超额收益
    AEI = daily_ratio_alpha.mean()
    # 超额收益最大回撤
    cumulative_alpha = normalize_cumulative(daily_ratio_alpha)
    max_return_alpha = cumulative_alpha.cummax()
    maxdrawdown_alpha = np.min((cumulative_alpha - max_return_alpha) / max_return_alpha)   
    # 超额收益夏普比率
    perf_stats_alpha = pf.timeseries.perf_stats((daily_ratio_alpha)).to_frame(name='all')
    sharp_ratio_alpha = perf_stats_alpha.loc['Sharpe ratio'][0]
    
    # 日胜率
    daily_win = port_pnl > market_pnl  # 返回布尔序列（True表示跑赢）
    total_days = len(daily_win)
    win_days = daily_win.sum()  # True的个数即为赢的天数
    daily_win_rate = (win_days / total_days) 
    
    # 信息比率
    daily_stdv = (port_pnl - market_pnl).std()
    annualized_stdv = daily_stdv * np.sqrt(250)    
    information_ratio = \
        (perf_stats.loc['Annual return'][0] - perf_stats_market.loc['Annual return'][0])/annualized_stdv
    
    # 胜率、盈亏比、盈利次数、亏损次数
    wins = [x for x in strategy.trades if x > 0]  # 盈利交易
    losses = [x for x in strategy.trades if x <= 0] # 亏损交易
    total_trades = len(strategy.trades)
    win_rate = len(wins) / total_trades if total_trades > 0 else 0
    total_profit = sum(wins)
    total_loss = abs(sum(losses))
    profit_factor = total_profit / total_loss if total_loss > 0 else float('inf')    
    
    stats_all.loc['策略收益'] = perf_stats.loc['Cumulative returns'][0]
    stats_all.loc['策略年化收益'] = perf_stats.loc['Annual return'][0]
    stats_all.loc['超额收益'] = cumulative_alpha.iloc[-1] - 1
    stats_all.loc['基准收益'] = perf_stats_market.loc['Cumulative returns'][0]
    stats_all.loc['阿尔法'] = alpha
    stats_all.loc['贝塔'] = beta
    stats_all.loc['夏普比率'] = perf_stats.loc['Sharpe ratio'][0]
    stats_all.loc['胜率'] = win_rate
    stats_all.loc['盈亏比'] = profit_factor
    stats_all.loc['最大回撤'] = abs(perf_stats.loc['Max drawdown'][0])
    stats_all.loc['最大回撤时段'] = max_drawdown_period
    stats_all.loc['索提诺比率'] = perf_stats.loc['Sortino ratio'][0]
    stats_all.loc['日均超额收益'] = AEI
    stats_all.loc['超额收益最大回撤'] = abs(maxdrawdown_alpha)
    stats_all.loc['超额收益夏普比率'] = sharp_ratio_alpha
    stats_all.loc['日胜率'] = daily_win_rate
    stats_all.loc['盈利次数'] = len(wins)
    stats_all.loc['亏损次数'] = len(losses)
    stats_all.loc['信息比率'] = information_ratio
    stats_all.loc['策略波动率'] = port_volatility
    stats_all.loc['基准波动率'] = market_volatility

    percentage_fields = ['策略收益', '策略年化收益', '超额收益', '基准收益',
                         '日均超额收益', '最大回撤', '超额收益最大回撤']
    integer_fields = ['盈利次数', '亏损次数']

    yearly_performance = get_yearly_performance(port_pnl)
    monthly_returns_matrix = get_monthly_returns_matrix(port_pnl)
    
    year_rows = max(len(yearly_performance), 1)
    fig_height = max(10.5, 8.8 + year_rows * 0.35)

    # plot
    fig = plt.figure(figsize=(19, fig_height), facecolor='white')
    gs = fig.add_gridspec(
        2, 2,
        width_ratios=[1.15, 2.85],
        height_ratios=[7.0, max(2.2, year_rows * 0.78)],
        wspace=0.08,
        hspace=0.14,
    )
    ax0 = fig.add_subplot(gs[0, 0])
    ax1 = fig.add_subplot(gs[0, 1])
    ax_year = fig.add_subplot(gs[1, :])

    since, until = port_pnl.index[0].strftime('%Y%m%d'), port_pnl.index[-1].strftime('%Y%m%d')
    since_label, until_label = port_pnl.index[0].strftime('%Y-%m-%d'), port_pnl.index[-1].strftime('%Y-%m-%d')
    title_text = fig_name if len(fig_name) > 0 else '回测分析'

    fig.suptitle(title_text, x=0.04, y=0.992, ha='left', fontsize=20, fontweight='bold', color='#0f172a')
    fig.text(
        0.04, 0.941,
        f'回测区间：{since_label} 至 {until_label}',
        ha='left', va='top', fontsize=10.5, color='#64748b'
    )

    # 左侧摘要面板
    ax0.axis('off')

    summary_fields = [field for field in stats_index if field != '最大回撤时段']
    left_count = int(np.ceil(len(summary_fields) / 2))
    card_width = 0.425
    x_positions = [0.03, 0.535]
    grid_top = 0.985
    period_bottom = 0.035
    period_height = 0.135
    grid_bottom = period_bottom + period_height + 0.03
    y_gap = 0.018
    card_height = (grid_top - grid_bottom - (left_count - 1) * y_gap) / left_count

    for idx, field in enumerate(summary_fields):
        if idx < left_count:
            col = 0
            row = idx
        else:
            col = 1
            row = idx - left_count

        x0 = x_positions[col]
        y0 = grid_top - (row + 1) * card_height - row * y_gap
        highlight_facecolor = '#f8fafc'
        if field in ['策略收益', '策略年化收益']:
            highlight_facecolor = '#eff6ff'
        elif field in ['最大回撤', '超额收益最大回撤']:
            highlight_facecolor = '#fef2f2'
        elif field in ['夏普比率', '信息比率']:
            highlight_facecolor = '#ecfeff'

        card = FancyBboxPatch(
            (x0, y0), card_width, card_height,
            boxstyle='round,pad=0.004,rounding_size=0.02',
            transform=ax0.transAxes,
            facecolor=highlight_facecolor,
            edgecolor='#e2e8f0',
            linewidth=0.9,
        )
        ax0.add_patch(card)
        ax0.text(
            x0 + 0.02, y0 + card_height * 0.50, field,
            transform=ax0.transAxes,
            ha='left', va='center',
            fontsize=8.8, color='#64748b'
        )
        ax0.text(
            x0 + card_width - 0.02, y0 + card_height * 0.50, format_stat_value(field, stats_all.loc[field]),
            transform=ax0.transAxes,
            ha='right', va='center',
            fontsize=10.8, fontweight='bold', color='#0f172a'
        )

    period_card = FancyBboxPatch(
        (0.03, period_bottom), 0.93, period_height,
        boxstyle='round,pad=0.006,rounding_size=0.02',
        transform=ax0.transAxes,
        facecolor='#eef4ff',
        edgecolor='#bfdbfe',
        linewidth=1.0,
    )
    ax0.add_patch(period_card)
    ax0.text(
        0.055, period_bottom + period_height * 0.70, '最大回撤时段',
        transform=ax0.transAxes,
        ha='left', va='center',
        fontsize=10, fontweight='bold', color='#1e3a8a'
    )
    ax0.text(
        0.055, period_bottom + period_height * 0.35, format_period_text(max_drawdown_start, max_drawdown_end),
        transform=ax0.transAxes,
        ha='left', va='center',
        fontsize=9.2, color='#1e40af'
        )

    # 右侧累计收益与回撤曲线
    ax1.set_facecolor('white')
    ax1.set_zorder(2)
    ax1.patch.set_alpha(0)
    ax_dd_bg = ax1.twinx()
    ax_dd_bg.set_zorder(0)
    ax_dd_bg.set_facecolor('none')
    ax_dd_bg.patch.set_alpha(0)
    ax_dd_bg.set_yticks([])
    for spine in ax_dd_bg.spines.values():
        spine.set_visible(False)
    ax_dd = ax1.twinx()
    ax_dd.set_zorder(3)
    ax_dd.set_facecolor('none')
    ax_dd.patch.set_alpha(0)

    strategy_color = '#f5c000'
    benchmark_color = '#ff3b30'
    drawdown_color = '#7eb6e6'

    strategy_line, = ax1.plot(
        cumulative.index, cumulative.values,
        color=strategy_color, lw=2.4, label='策略累计收益', zorder=3
    )
    benchmark_line, = ax1.plot(
        cumulative_market.index, cumulative_market.values,
        color=benchmark_color, lw=1.5, linestyle='-', label='基准累计收益', zorder=2
    )
    ax1.fill_between(
        cumulative.index, cumulative.values, 1,
        color='#fff3bf', alpha=0.15, zorder=1
    )
    ax_dd_bg.fill_between(
        drawdown.index, drawdown.values, 0,
        color='#8fb7db', alpha=0.55, zorder=0
    )
    drawdown_line, = ax_dd_bg.plot(
        drawdown.index, drawdown.values,
        color=drawdown_color, lw=0.6, alpha=0.95, label='策略回撤', zorder=0.2
    )
    drawdown_floor = min(drawdown.min() * 1.02, -0.001)
    ax_dd.set_ylim(drawdown_floor, 0)
    ax_dd_bg.set_ylim(drawdown_floor, 0)
    ax_dd_bg.set_xbound(lower=cumulative.index.min(), upper=cumulative.index.max())

    if not pd.isna(max_drawdown_start) and not pd.isna(max_drawdown_end):
        ax1.scatter(max_drawdown_end, cumulative.loc[max_drawdown_end], color=drawdown_color, s=38, zorder=4)
        ax1.annotate(
            f"最大回撤低点\n{abs(drawdown.min()) * 100:.2f}%",
            xy=(max_drawdown_end, cumulative.loc[max_drawdown_end]),
            xytext=(12, -8),
            textcoords='offset points',
            fontsize=9.2,
            color='black',
            bbox=dict(boxstyle='round,pad=0.28', facecolor='white', edgecolor='#9ec5ea', alpha=0.95),
            arrowprops=dict(arrowstyle='->', color='#7eb6e6', lw=1.0),
        )

    # ax1.grid(axis='y', linestyle='--', linewidth=0.8, color='#dbe3ef', alpha=0.95)
    ax1.grid(axis='x', linestyle='-', linewidth=0.45, color='#eef2f7', alpha=0.75)
    ax1.axhline(1, color='#cbd5e1', linewidth=1.0, linestyle=':')
    ax_dd.axhline(0, color='#d8dee9', linewidth=1.0, linestyle=':')

    ax1.set_xbound(lower=cumulative.index.min(), upper=cumulative.index.max())
    ax1.set_ylabel('累计收益', fontsize=10.5, color='black')
    ax_dd.set_ylabel('回撤', fontsize=10.5, color='black', rotation=270, labelpad=14)
    ax_dd.yaxis.set_label_position('right')
    ax_dd.yaxis.tick_right()
    ax1.yaxis.set_major_formatter(FuncFormatter(lambda y, _: f'{(y - 1) * 100:.0f}%'))
    ax_dd.yaxis.set_major_formatter(FuncFormatter(lambda y, _: f'{y * 100:.0f}%'))
    ax1.tick_params(axis='x', labelsize=10, colors='black')
    ax1.tick_params(axis='y', labelsize=10, colors='black')
    ax_dd.tick_params(axis='y', labelsize=10, colors='black')

    ax1.spines['top'].set_visible(False)
    ax1.spines['right'].set_visible(False)
    ax1.spines['left'].set_color('#cbd5e1')
    ax1.spines['bottom'].set_color('#cbd5e1')
    ax_dd.spines['top'].set_visible(False)
    ax_dd.spines['left'].set_visible(False)
    ax_dd.spines['right'].set_color('#cbd5e1')

    legend = ax1.legend(
        [strategy_line, benchmark_line, drawdown_line],
        ['策略累计收益', '基准累计收益', '策略回撤'],
        loc='upper left',
        bbox_to_anchor=(0.012, 0.91),
        frameon=True,
        fontsize=10.5,
        ncol=1,
    )
    legend.get_frame().set_facecolor('white')
    # legend.get_frame().set_alpha(1)
    legend.get_frame().set_edgecolor('#e5e7eb')
    legend.get_frame().set_linewidth(0.8)
    for text in legend.get_texts():
        text.set_color('black')

    # 底部年度表现表
    ax_year.axis('off')
    ax_year.text(0.02, 0.94, '年度表现', transform=ax_year.transAxes,
                 ha='left', va='center', fontsize=13, fontweight='bold', color='#0f172a')

    if yearly_performance.empty:
        ax_year.text(
            0.02, 0.5, '回测区间内没有可展示的年度数据。',
            transform=ax_year.transAxes, ha='left', va='center',
            fontsize=10.5, color='#64748b'
        )
    else:
        yearly_table_display = yearly_performance[['收益', '最大回撤', '最大回撤时段']].copy()
        yearly_table_display['收益'] = yearly_table_display['收益'].map(lambda x: f"{x * 100:.2f}%")
        yearly_table_display['最大回撤'] = yearly_table_display['最大回撤'].map(lambda x: f"{x * 100:.2f}%")
        yearly_table_data = [
            [year, row['收益'], row['最大回撤'], row['最大回撤时段']]
            for year, row in yearly_table_display.iterrows()
        ]

        yearly_table = ax_year.table(
            cellText=yearly_table_data,
            colLabels=['年份', '收益', '最大回撤', '最大回撤时段'],
            cellLoc='center',
            colLoc='center',
            bbox=[0.02, 0.05, 0.96, 0.76],
            colWidths=[0.10, 0.16, 0.16, 0.54],
        )
        yearly_table.auto_set_font_size(False)
        yearly_table.set_fontsize(10.3)
        yearly_table.scale(1.0, 1.45)

        for (row, col), cell in yearly_table.get_celld().items():
            if row == 0:
                cell.set_facecolor('#dbeafe')
                cell.set_text_props(fontweight='bold', color='#0f172a')
                cell.set_edgecolor('#bfdbfe')
                cell.set_linewidth(0.9)
                continue

            cell.set_edgecolor('#e2e8f0')
            cell.set_linewidth(0.8)
            cell.set_facecolor('#ffffff' if row % 2 == 1 else '#f8fafc')

            if col == 3:
                cell.get_text().set_fontsize(9.2)
                cell.get_text().set_color('#334155')
            else:
                cell.get_text().set_color('#0f172a')

    fig.subplots_adjust(top=0.89, bottom=0.07, left=0.04, right=0.98)

    if len(fig_name)>0:
        plt.savefig(fig_name + '_backtest_' + since + '_' + until + '.png',
                    dpi=160, bbox_inches='tight', facecolor=fig.get_facecolor())
    else:
        plt.savefig('backtest_' + since + '_' + until + '.png',
                    dpi=160, bbox_inches='tight', facecolor=fig.get_facecolor())

    heatmap_fig_height = max(4.5, 1.6 + 0.68 * max(len(monthly_returns_matrix), 1))
    fig_heatmap, ax_heatmap = plt.subplots(figsize=(15.5, heatmap_fig_height), facecolor='white')

    heatmap_title = (f'{fig_name} 月度收益热力图' if len(fig_name) > 0 else '月度收益热力图')
    heatmap_display = monthly_returns_matrix * 100
    annot_matrix = heatmap_display.applymap(lambda x: '' if pd.isna(x) else f'{x:.1f}%')
    cmap = sns.diverging_palette(10, 130, s=95, l=55, as_cmap=True)
    cmap = cmap.reversed()
    vmax = np.nanmax(np.abs(heatmap_display.values)) if np.isfinite(heatmap_display.values).any() else 1
    vmax = max(vmax, 1)

    sns.heatmap(
        heatmap_display,
        ax=ax_heatmap,
        cmap=cmap,
        center=0,
        vmin=-vmax,
        vmax=vmax,
        annot=annot_matrix,
        fmt='',
        linewidths=0.8,
        linecolor='white',
        mask=heatmap_display.isna(),
        cbar_kws={'label': '月收益率 (%)', 'shrink': 0.85},
    )

    ax_heatmap.set_title(heatmap_title, loc='left', fontsize=15, fontweight='bold', color='#0f172a', pad=14)
    ax_heatmap.set_xlabel('')
    ax_heatmap.set_ylabel('')
    ax_heatmap.tick_params(axis='x', labelrotation=0, labelsize=10, colors='black')
    ax_heatmap.tick_params(axis='y', labelrotation=0, labelsize=10, colors='black')

    for spine in ax_heatmap.spines.values():
        spine.set_visible(False)

    fig_heatmap.tight_layout()

    if len(fig_name)>0:
        fig_heatmap.savefig(fig_name + '_monthly_heatmap_' + since + '_' + until + '.png',
                            dpi=160, bbox_inches='tight', facecolor=fig_heatmap.get_facecolor())
    else:
        fig_heatmap.savefig('monthly_heatmap_' + since + '_' + until + '.png',
                            dpi=160, bbox_inches='tight', facecolor=fig_heatmap.get_facecolor())
        
    plt.show()
    
    return stats_all