# Day 25 (100天计划): 低估值策略（PB-ROE框架）
# 核心：低PB=便宜，高ROE=优质，两者叠加寻找"便宜的好公司"

# ==================== 可切换参数（在这里修改）====================
VERSION = 'v1'  # 'v1': 纯低PB | 'v2': 低PB+高ROE过滤 | 'v3': PB-ROE打分 | 'v4': PB-ROE+换手率
# ================================================================

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

"""
回测参数：2020-01-01 至 2026-02-01，100万，沪深300，月度调仓，持仓20只
基准：沪深300（14.88%）

PB-ROE框架理论：
  PB（市净率）= 市场价格 / 每股净资产
    PB<1：市场认为公司资产不值账面价值（悲观定价）
    PB越低 = 越"便宜"

  ROE（净资产收益率）= 净利润 / 净资产
    衡量公司用净资产赚钱的能力
    ROE越高 = 资产质量越好

  核心逻辑（巴菲特风格）：
    高ROE但低PB → 市场低估了资产的盈利能力 → 长期回归价值
    PB = ROE / 期望回报率（Gordon模型简化）
    若ROE=15%，市场要求回报=10%，则合理PB=1.5
    若当前PB=0.8 < 1.5，则明显低估

  陷阱识别：
    纯低PB可能是"价值陷阱"——PB低因为ROE也低（银行/地产/资源股）
    PB-ROE双维度过滤排除这类陷阱

V1: 纯低PB Top20（最简单，易有价值陷阱）
V2: 低PB(PB<2) + ROE>10%（双维度过滤，排除低质量低估）
V3: PB-ROE综合打分（低PB得分×50% + 高ROE得分×50%，等权合成）
V4: PB-ROE打分 + 换手率（三因子，排除流动性陷阱）

注：换手率使用 get_fundamentals(valuation.turnover_ratio) 稳定接口
    修复 Day20-22 反复出现的 get_price 换手率空值问题
"""


def initialize(context):
    g.stock_num = 20
    g.index = '000300.XSHG'
    set_option('use_real_price', True)
    log.set_level('order', 'error')
    run_monthly(trade, 1)


# ==================== 数据获取 ====================

def get_pb_roe(stocks, date):
    """获取 PB 和 ROE"""
    try:
        q = query(
            valuation.code,
            valuation.pb_ratio,
            indicator.roe
        ).filter(
            valuation.code.in_(stocks),
            valuation.pb_ratio > 0,  # 排除负PB（净资产为负的公司）
            indicator.roe > 0        # 排除亏损
        )
        df = get_fundamentals(q, date=date)
        if df.empty:
            return pd.DataFrame()
        return df.set_index('code')[['pb_ratio', 'roe']]
    except Exception as e:
        log.error(f"PB/ROE获取失败: {e}")
        return pd.DataFrame()


def get_turnover_ratio(stocks, date):
    """
    获取换手率（%）
    使用 get_fundamentals 稳定接口（修复Day20-22的get_price空值问题）
    """
    try:
        q = query(
            valuation.code,
            valuation.turnover_ratio
        ).filter(
            valuation.code.in_(stocks),
            valuation.turnover_ratio > 0
        )
        df = get_fundamentals(q, date=date)
        if df.empty:
            return pd.Series(dtype=float)
        return df.set_index('code')['turnover_ratio']
    except Exception as e:
        log.error(f"换手率获取失败: {e}")
        return pd.Series(dtype=float)


def to_score(series, higher_is_better=True):
    """因子值 → 百分位分数 (0, 100]"""
    s = series.dropna()
    if s.empty or s.std() == 0:
        return pd.Series(50.0, index=s.index)
    if higher_is_better:
        return s.rank(pct=True) * 100
    else:
        return s.rank(ascending=False, pct=True) * 100


# ==================== 交易 ====================

def trade(context):
    stocks = get_index_stocks(g.index)
    dt = context.current_dt
    target = []

    # 所有版本都需要 PB 和 ROE
    df = get_pb_roe(stocks, dt)
    if df.empty:
        log.warn("PB/ROE数据为空，跳过")
        return

    if VERSION == 'v1':
        # ── V1：纯低PB Top20 ──
        # 最简单的低估值策略，直接按PB从小到大排序
        target = df['pb_ratio'].nsmallest(g.stock_num).index.tolist()
        log.info(f"[V1] 最小PB Top3: {df['pb_ratio'].nsmallest(3).round(2).to_dict()}")

    elif VERSION == 'v2':
        # ── V2：低PB + ROE>10%（双维度过滤）──
        # 排除"低PB但ROE也低"的价值陷阱
        # 典型陷阱：传统银行（PB=0.5，ROE=8%），低估值但盈利能力差
        filtered = df[(df['pb_ratio'] < 2.0) & (df['roe'] > 10)]
        if len(filtered) < g.stock_num:
            # 不足时放宽到ROE>5%
            filtered = df[(df['pb_ratio'] < 3.0) & (df['roe'] > 5)]
        target = filtered['pb_ratio'].nsmallest(g.stock_num).index.tolist()
        log.info(f"[V2] PB<2+ROE>10%过滤后: {len(filtered)}只，选最小PB {len(target)}只")

    elif VERSION == 'v3':
        # ── V3：PB-ROE综合打分（核心版本）──
        # 低PB得分（PB越小分越高）× 50% + 高ROE得分（ROE越大分越高）× 50%
        # 捕获"又便宜又优质"的股票，而非单纯便宜或单纯优质
        pb_score  = to_score(df['pb_ratio'], higher_is_better=False)  # PB小=分高
        roe_score = to_score(df['roe'],      higher_is_better=True)   # ROE大=分高

        common = pb_score.index.intersection(roe_score.index)
        total  = pb_score[common] * 0.5 + roe_score[common] * 0.5
        target = total.sort_values(ascending=False).head(g.stock_num).index.tolist()
        log.info(f"[V3] PB-ROE等权打分，候选: {len(common)}只")

    elif VERSION == 'v4':
        # ── V4：PB-ROE打分 + 换手率（三因子）──
        # 在PB-ROE基础上加入换手率，排除流动性陷阱（低估值但无人交易）
        pb_score  = to_score(df['pb_ratio'], higher_is_better=False)
        roe_score = to_score(df['roe'],      higher_is_better=True)

        tr = get_turnover_ratio(df.index.tolist(), dt)
        tr_score = to_score(tr, higher_is_better=True)  # 换手率高=流动性好=分高

        common = pb_score.index.intersection(roe_score.index).intersection(tr_score.index)
        if len(common) < g.stock_num:
            # 换手率数据不足时退化为V3
            log.warn(f"[V4] 换手率交集{len(common)}只不足，退化为V3")
            common = pb_score.index.intersection(roe_score.index)
            total  = pb_score[common] * 0.5 + roe_score[common] * 0.5
        else:
            total = pb_score[common] * 0.4 + roe_score[common] * 0.4 + tr_score[common] * 0.2

        target = total.sort_values(ascending=False).head(g.stock_num).index.tolist()
        log.info(f"[V4] PB×0.4+ROE×0.4+换手率×0.2，候选: {len(common)}只")

    else:
        return

    if not target:
        log.warn(f"[{VERSION}] 无符合条件股票，跳过")
        return

    # ── 执行交易 ──
    for stock in list(context.portfolio.positions.keys()):
        if stock not in target:
            order_target(stock, 0)

    w = 1.0 / len(target)
    for stock in target:
        order_target_value(stock, context.portfolio.total_value * w)
