# Day 12: 因子预处理 - 去极值、标准化、中性化
# 测试PCF因子的4个版本

# ==================== 可切换参数（在这里修改）====================
VERSION = 'v1'  # 'v1': 原始PCF | 'v2': 去极值+标准化 | 'v3': 行业中性化 | 'v4': 市值中性化
# ================================================================

import pandas as pd
import numpy as np
from jqdata import *

# ==================== 测试说明 ====================
"""
回测参数：
- 时间：2020-01-01 至 2026-02-01
- 初始资金：100万
- 股票池：沪深300
- 调仓：月初
- 持仓：20只

测试版本：
1. V1（原始PCF）：
   - 直接用PCF排序
   - 对应Day 1的结果：38.06%

2. V2（去极值+标准化）：
   - 百分位去极值（5%-95%）
   - Z-score标准化
   - 预期：略优于V1（去除极端值干扰）

3. V3（行业中性化）：
   - 在V2基础上行业中性化
   - 预期：Alpha最高（剔除行业beta）
   - 但绝对收益可能低于V1（失去行业轮动收益）

4. V4（市值中性化）：
   - 在V2基础上市值中性化
   - 预期：剔除小盘效应
   - 适合大资金（避免市值风格暴露）

如何测试：
1. 修改文件开头的 VERSION = 'v1' 为 'v2', 'v3', 'v4'
2. 分别回测4个版本
3. 记录每个版本的收益、Alpha、Beta、Sharpe、回撤
4. 对比分析预处理的效果

关键指标对比：
- 绝对收益：V1可能最高（保留行业beta）
- Alpha：V3应该最高（行业中性）
- Beta：V3应该最低（剔除行业暴露）
- Sharpe：V3可能最高（风险调整后收益）

学习重点：
1. 理解去极值的必要性（对比V1 vs V2）
2. 理解中性化的作用（对比V2 vs V3）
3. 理解Alpha vs 绝对收益的区别
4. 掌握因子预处理的完整流程
"""

def initialize(context):
    set_params()
    set_backtest()
    run_monthly(trade, 1)

def set_params():
    g.stock_num = 20
    g.index = '000300.XSHG'  # 沪深300

def set_backtest():
    set_option('use_real_price', True)
    log.set_level('order', 'error')

# ==================== 因子预处理函数 ====================

def winsorize_percentile(series, lower=0.05, upper=0.95):
    """
    百分位去极值
    """
    lower_bound = series.quantile(lower)
    upper_bound = series.quantile(upper)
    return series.clip(lower_bound, upper_bound)

def winsorize_mad(series, n=3):
    """
    MAD去极值
    """
    median = series.median()
    mad = (series - median).abs().median()
    lower_bound = median - n * mad
    upper_bound = median + n * mad
    return series.clip(lower_bound, upper_bound)

def standardize(series):
    """
    Z-score标准化
    """
    return (series - series.mean()) / series.std()

def neutralize_industry(factor_df, stocks, date):
    """
    行业中性化（使用聚宽行业数据）
    """
    # 方法1：使用get_industry_stocks获取行业成分股（反向推导）
    # 方法2：使用jqdata的行业分类表

    # 尝试使用聚宽的行业分类
    from jqdata import get_industries

    industry_dict = {}

    # 获取申万一级行业列表
    try:
        industries = get_industries('sw_l1', date=date)

        # 对每个行业，获取成分股
        for ind_code in industries.index:
            ind_name = industries.loc[ind_code]['name']
            ind_stocks = get_industry_stocks(ind_code, date=date)

            # 找出在我们股票池中的股票
            stocks_in_industry = [s for s in stocks if s in ind_stocks]

            if stocks_in_industry:
                industry_dict[ind_name] = stocks_in_industry

        log.info(f"[行业中性化] 共{len(industry_dict)}个行业")

    except Exception as e:
        log.warn(f"[行业中性化] 获取行业分类失败: {e}，返回全局标准化结果")
        return standardize(factor_df)

    # 如果行业分类失败
    if len(industry_dict) <= 1:
        log.warn("[行业中性化] 行业数量<=1，返回全局标准化结果")
        return standardize(factor_df)

    # 行业内标准化
    neutral_factor = pd.Series(index=stocks, dtype=float)

    for industry, stocks_in_industry in industry_dict.items():
        if len(stocks_in_industry) > 1:
            # 获取这些股票的因子值
            common_stocks = [s for s in stocks_in_industry if s in factor_df.index]
            if common_stocks:
                factor_values = factor_df.loc[common_stocks]
                neutral_factor.loc[common_stocks] = standardize(factor_values)
        elif len(stocks_in_industry) == 1:
            # 单只股票的行业，因子值设为0
            neutral_factor.loc[stocks_in_industry[0]] = 0.0

    # 处理未分配到行业的股票（设为0）
    for stock in stocks:
        if pd.isna(neutral_factor.loc[stock]):
            neutral_factor.loc[stock] = 0.0

    return neutral_factor

def neutralize_market_cap(factor_df, stocks, date):
    """
    市值中性化（回归法）
    """
    # 获取市值
    q = query(valuation.code, valuation.market_cap).filter(
        valuation.code.in_(stocks)
    )
    df_cap = get_fundamentals(q, date)
    df_cap = df_cap.set_index('code')

    # 取对数
    ln_cap = np.log(df_cap['market_cap'])

    # 对齐索引
    common_stocks = factor_df.index.intersection(ln_cap.index)
    factor_values = factor_df.loc[common_stocks]
    ln_cap_values = ln_cap.loc[common_stocks]

    # 线性回归：factor = α + β × ln(cap) + ε
    # 使用numpy的最小二乘法
    X = ln_cap_values.values.reshape(-1, 1)
    y = factor_values.values

    # 添加截距项
    X_with_intercept = np.column_stack([np.ones(len(X)), X])

    # 求解 β = (X'X)^(-1) X'y
    try:
        beta = np.linalg.lstsq(X_with_intercept, y, rcond=None)[0]
        # 残差 = factor - (α + β × ln(cap))
        residuals = y - X_with_intercept @ beta
        neutral_factor = pd.Series(residuals, index=common_stocks)
    except:
        # 回归失败，返回原因子
        neutral_factor = factor_values

    return neutral_factor

# ==================== 因子计算函数 ====================

def get_pcf_factor(stocks, date, version='v1'):
    """
    计算PCF因子（不同预处理版本）

    version:
        'v1': 原始PCF（Day 1基准）
        'v2': 去极值 + 标准化
        'v3': 去极值 + 标准化 + 行业中性化
        'v4': 去极值 + 标准化 + 市值中性化
    """
    # 获取PCF数据
    q = query(valuation.code, valuation.pcf_ratio).filter(
        valuation.code.in_(stocks),
        valuation.pcf_ratio > 0
    )
    df = get_fundamentals(q, date)

    if df.empty:
        return pd.Series(dtype=float)

    df = df.set_index('code')
    pcf = df['pcf_ratio']

    # V1: 原始PCF（低PCF排前面，所以取负数）
    if version == 'v1':
        return -pcf

    # V2: 去极值 + 标准化
    if version == 'v2':
        pcf_winsorized = winsorize_percentile(pcf, lower=0.05, upper=0.95)
        pcf_standardized = standardize(pcf_winsorized)
        return -pcf_standardized  # 低PCF排前面

    # V3: 去极值 + 标准化 + 行业中性化
    if version == 'v3':
        pcf_winsorized = winsorize_percentile(pcf, lower=0.05, upper=0.95)
        pcf_standardized = standardize(pcf_winsorized)
        pcf_neutral = neutralize_industry(-pcf_standardized, pcf.index.tolist(), date)
        return pcf_neutral

    # V4: 去极值 + 标准化 + 市值中性化
    if version == 'v4':
        pcf_winsorized = winsorize_percentile(pcf, lower=0.05, upper=0.95)
        pcf_standardized = standardize(pcf_winsorized)
        pcf_neutral = neutralize_market_cap(-pcf_standardized, pcf.index.tolist(), date)
        return pcf_neutral

    return pd.Series(dtype=float)

# ==================== 交易函数 ====================

def trade(context):
    # 获取股票池
    stocks = get_index_stocks(g.index)

    # 计算因子（使用文件开头定义的VERSION）
    factor = get_pcf_factor(stocks, context.current_dt, version=VERSION)

    if factor.empty:
        return

    # 按因子值排序，选前20只
    factor_sorted = factor.sort_values(ascending=False)
    target_stocks = factor_sorted.head(g.stock_num).index.tolist()

    # 获取当前持仓
    current_stocks = list(context.portfolio.positions.keys())

    # 卖出不在目标池的股票
    for stock in current_stocks:
        if stock not in target_stocks:
            order_target(stock, 0)

    # 等权买入目标股票
    if len(target_stocks) > 0:
        weight = 1.0 / len(target_stocks)
        for stock in target_stocks:
            order_target_value(stock, context.portfolio.total_value * weight)
