"""Serialization and I/O for the FactorLibrary.

Provides save/load to JSON + optional binary signal cache (.npz),
CSV export, formula export, and import of the 110 factors from the paper.
"""

from __future__ import annotations

import csv
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional, Union

import numpy as np

from src.factorminer.core.factor_library import Factor, FactorLibrary

logger = logging.getLogger(__name__)


# ======================================================================
# Save / Load
# ======================================================================


def save_library(
    library: FactorLibrary,
    path: Union[str, Path],
    save_signals: bool = True,
) -> None:
    """Save a FactorLibrary to disk (仅保存 JSON 元数据).

    Parameters
    ----------
    library : FactorLibrary
    path : str or Path
        Base path (without extension). E.g. ``"output/my_library"`` produces
        ``output/my_library.json``.
    save_signals : bool
        已废弃，始终忽略，不再写入 .npz 信号缓存。
    """
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)

    # -- Metadata JSON --
    meta = {
        "correlation_threshold": library.correlation_threshold,
        "ic_threshold": library.ic_threshold,
        "next_id": library._next_id,
        "factors": [f.to_dict() for f in library.list_factors()],
    }
    # 内存优化：相关性矩阵不再保存到 JSON
    # if library.correlation_matrix is not None:
    #     meta["correlation_matrix"] = library.correlation_matrix.tolist()

    json_path = path.with_suffix(".json")
    with open(json_path, "w") as fp:
        json.dump(meta, fp, indent=2)
    logger.info("Saved library metadata to %s (%d factors)", json_path, library.size)

    # -- Binary signal cache (已禁用) --
    print("[library_io] 信号缓存已禁用，仅保存 JSON 元数据")
    if save_signals:
        logger.info("save_signals 参数已废弃，信号缓存不再写入")


def load_library(path: Union[str, Path]) -> FactorLibrary:
    """Load a FactorLibrary from disk.

    Parameters
    ----------
    path : str or Path
        Base path (without extension). Will look for ``<path>.json``.

    Returns
    -------
    FactorLibrary
    """
    path = Path(path)
    json_path = path.with_suffix(".json")

    with open(json_path, "r") as fp:
        meta = json.load(fp)

    library = FactorLibrary(
        correlation_threshold=meta.get("correlation_threshold", 0.5),
        ic_threshold=meta.get("ic_threshold", 0.04),
    )
    library._next_id = meta.get("next_id", 1)

    # Restore factors
    for fd in meta.get("factors", []):
        factor = Factor.from_dict(fd)
        if factor.formula.strip().startswith("# TODO"):
            factor.metadata["unsupported"] = True
        # 内存优化：signals 不再从 JSON 恢复，设为 None
        factor.signals = None
        library.factors[factor.id] = factor

    # 内存优化：相关性矩阵不再从 JSON 恢复（按需重算）
    # if "correlation_matrix" in meta and meta["correlation_matrix"] is not None:
    #     library.correlation_matrix = np.array(
    #         meta["correlation_matrix"], dtype=np.float64
    #     )

    logger.info("Loaded library from %s (%d factors)", json_path, library.size)
    return library


# ======================================================================
# Export utilities
# ======================================================================


def export_csv(library: FactorLibrary, path: Union[str, Path]) -> None:
    """Export the factor table to CSV.

    Columns: ID, Name, Formula, Category, IC_Mean, ICIR, IC_Win_Rate,
    Max_Correlation, Batch, Admission_Date
    """
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)

    fieldnames = [
        "ID",
        "Name",
        "Formula",
        "Category",
        "IC_Mean",
        "ICIR",
        "IC_Win_Rate",
        "Max_Correlation",
        "Batch",
        "Admission_Date",
    ]

    with open(path, "w", newline="") as fp:
        writer = csv.DictWriter(fp, fieldnames=fieldnames)
        writer.writeheader()
        for f in library.list_factors():
            writer.writerow(
                {
                    "ID": f.id,
                    "Name": f.name,
                    "Formula": f.formula,
                    "Category": f.category,
                    "IC_Mean": f"{f.ic_mean:.6f}",
                    "ICIR": f"{f.icir:.6f}",
                    "IC_Win_Rate": f"{f.ic_win_rate:.4f}",
                    "Max_Correlation": f"{f.max_correlation:.4f}",
                    "Batch": f.batch_number,
                    "Admission_Date": f.admission_date,
                }
            )

    logger.info("Exported %d factors to %s", library.size, path)


def export_formulas(library: FactorLibrary, path: Union[str, Path]) -> None:
    """Export just the formulas for reproduction.

    One formula per line, prefixed with the factor ID and name.
    Format: ``ID | Name | Formula``
    """
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)

    with open(path, "w") as fp:
        fp.write("# FactorMiner Library Formulas\n")
        fp.write("# ID | Name | Formula\n")
        fp.write(f"# Total: {library.size} factors\n")
        fp.write("#" + "-" * 78 + "\n")
        for f in library.list_factors():
            fp.write(f"{f.id:04d} | {f.name} | {f.formula}\n")

    logger.info("Exported %d formulas to %s", library.size, path)


# ======================================================================
# Paper factor catalog (110 factors from Appendix P)
# ======================================================================

# Representative subset of the 110 factors discovered by FactorMiner.
# Each entry: (name, formula, category)
PAPER_FACTORS: List[Dict[str, str]] = [
    {
        "name": "Intraday Range Position",
        "formula": "(-cs_rank(((close - ts_min(close, 48)) / ((ts_max(close, 48) - ts_min(close, 48)) + 1e-8))))",
        "category": "Mean-reversion",
    },
    {
        "name": "Volume-Weighted Momentum",
        "formula": "(-cs_rank((ts_pct_change(close, 5) * (vol / ts_mean(vol, 20)))))",
        "category": "Momentum",
    },
    {
        "name": "Residual Volatility",
        "formula": "(-cs_rank(ts_std((close - ts_ema(close, 10)), 20)))",
        "category": "Volatility",
    },
    {
        "name": "Intraday Amplitude Ratio",
        "formula": "(-cs_rank(((high - low) / (close + 1e-8))))",
        "category": "Volatility",
    },
    {
        "name": "Volume Surprise",
        "formula": "(-cs_rank(((vol - ts_mean(vol, 20)) / (ts_std(vol, 20) + 1e-8))))",
        "category": "Volume",
    },
    {
        "name": "VWAP Deviation",
        "formula": "(-((close - (amount / vol)) / (amount / vol)))",
        "category": "VWAP",
    },
    {
        "name": "Short-term Reversal",
        "formula": "(-cs_rank(ts_pct_change(close, 3)))",
        "category": "Mean-reversion",
    },
    {
        "name": "Turnover Momentum",
        "formula": "(-cs_rank(ts_delta((amount / (vol + 1e-8)), 5)))",
        "category": "Turnover",
    },
    {
        "name": "High-Low Midpoint Reversion",
        "formula": "(-cs_rank((close - ((high + low) / 2))))",
        "category": "Mean-reversion",
    },
    # {
    #     "name": 'Rolling Beta Residual',
    #     "formula": '# TODO: Neg(CsRank(Resid($returns, Mean($returns, 20), 20)))',
    #     "category": 'Risk',
    # },
    # {
    #     "name": 'VWAP Slope',
    #     "formula": '# TODO: Neg(CsRank(TsLinRegSlope(Div(Sub($close, $vwap), $vwap), 10)))',
    #     "category": 'VWAP',
    # },
    {
        "name": "Accumulation-Distribution",
        "formula": "(-cs_rank(ts_sum(((((2 * close) - (high + low)) / ((high - low) + 1e-8)) * vol), 10)))",
        "category": "Volume",
    },
    {
        "name": "Relative Strength Index Deviation",
        "formula": "(-cs_rank((ts_mean(max_(ts_delta(close, 1), 0), 14) - ts_mean(abs(min_(ts_delta(close, 1), 0)), 14))))",
        "category": "Momentum",
    },
    {
        "name": "Price-Volume Correlation",
        "formula": "(-ts_corr(close, vol, 10))",
        "category": "Volume",
    },
    {
        "name": "Skewness of Returns",
        "formula": "(-cs_rank(ts_skew((close / ts_delay(close, 1) - 1), 20)))",
        "category": "Higher-moment",
    },
    {
        "name": "Kurtosis of Returns",
        "formula": "(-cs_rank(ts_kurt((close / ts_delay(close, 1) - 1), 20)))",
        "category": "Higher-moment",
    },
    {
        "name": "Volume-Weighted Return",
        "formula": "(-cs_rank((ts_sum(((close / ts_delay(close, 1) - 1) * vol), 10) / (ts_sum(vol, 10) + 1e-8))))",
        "category": "Volume",
    },
    {
        "name": "Close-to-High Ratio",
        "formula": "(-cs_rank(((high - close) / (high + 1e-8))))",
        "category": "Mean-reversion",
    },
    {
        "name": "Delayed Correlation Shift",
        "formula": "(-cs_rank((ts_corr(close, vol, 10) - ts_corr(ts_delay(close, 5), vol, 10))))",
        "category": "Volume",
    },
    {
        "name": "Exponential Momentum",
        "formula": "(-cs_rank((close - ts_ema(close, 20))))",
        "category": "Momentum",
    },
    {
        "name": "Range-Adjusted Volume",
        "formula": "(-cs_rank((vol / ((high - low) + 1e-8))))",
        "category": "Volume",
    },
    {
        "name": "Cumulative Return Rank",
        "formula": "(-cs_rank(ts_sum((close / ts_delay(close, 1) - 1), 10)))",
        "category": "Momentum",
    },
    {
        "name": "VWAP Momentum",
        "formula": "(-cs_rank(ts_pct_change((amount / vol), 5)))",
        "category": "VWAP",
    },
    {
        "name": "Bollinger Band Position",
        "formula": "(-cs_rank(((close - ts_mean(close, 20)) / (ts_std(close, 20) + 1e-8))))",
        "category": "Mean-reversion",
    },
    # {
    #     "name": 'Volume Decay Weighted',
    #     "formula": '# TODO: Neg(CsRank(Decay($volume, 10)))',
    #     "category": 'Volume',
    # },
    {
        "name": "Overnight Return",
        "formula": "(-cs_rank(((open - ts_delay(close, 1)) / (ts_delay(close, 1) + 1e-8))))",
        "category": "Overnight",
    },
    {
        "name": "Intraday Return",
        "formula": "(-cs_rank(((close - open) / (open + 1e-8))))",
        "category": "Intraday",
    },
    {
        "name": "Max Drawdown",
        "formula": "(-cs_rank(((close - ts_max(close, 20)) / (ts_max(close, 20) + 1e-8))))",
        "category": "Risk",
    },
    {
        "name": "Hurst Exponent Proxy",
        "formula": "(-cs_rank((ts_std((close / ts_delay(close, 1) - 1), 20) / (ts_std((close / ts_delay(close, 1) - 1), 5) + 1e-8))))",
        "category": "Volatility",
    },
    {
        "name": "Volume Imbalance",
        "formula": "(-cs_rank((ts_mean(vol, 5) - ts_mean(vol, 20))))",
        "category": "Volume",
    },
    {
        "name": "Weighted Close Position",
        "formula": "(-cs_rank((((2 * close) - (high + low)) / ((high - low) + 1e-8))))",
        "category": "Mean-reversion",
    },
    {
        "name": "Trend Intensity",
        "formula": "(-cs_rank((abs(ts_delta(close, 10)) / (ts_sum(abs(ts_delta(close, 1)), 10) + 1e-8))))",
        "category": "Trend",
    },
    {
        "name": "Return Dispersion",
        "formula": "(-cs_rank(ts_std((close / ts_delay(close, 1) - 1), 5)))",
        "category": "Volatility",
    },
    {
        "name": "VWAP Relative Strength",
        "formula": "(-cs_rank(((ts_mean(close, 5) - (amount / vol)) / ((amount / vol) + 1e-8))))",
        "category": "VWAP",
    },
    {
        "name": "Rank Reversal",
        "formula": "(-cs_rank((ts_rank(close, 10) - ts_rank(close, 30))))",
        "category": "Mean-reversion",
    },
    {
        "name": "Money Flow Index",
        "formula": "(-cs_rank((ts_sum((max_(ts_delta(close, 1), 0) * vol), 14) / (ts_sum((abs(ts_delta(close, 1)) * vol), 14) + 1e-8))))",
        "category": "Volume",
    },
    {
        "name": "Adaptive Momentum",
        "formula": "(-cs_rank((ts_pct_change(close, 10) * (ts_std((close / ts_delay(close, 1) - 1), 5) / (ts_std((close / ts_delay(close, 1) - 1), 20) + 1e-8)))))",
        "category": "Momentum",
    },
    # {
    #     "name": 'Volume Trend',
    #     "formula": '# TODO: Neg(CsRank(TsLinRegSlope($volume, 10)))',
    #     "category": 'Volume',
    # },
    {
        "name": "Price Acceleration",
        "formula": "(-cs_rank((ts_delta(close, 5) - ts_delta(ts_delay(close, 5), 5))))",
        "category": "Momentum",
    },
    {
        "name": "Realized Volatility Ratio",
        "formula": "(-cs_rank((ts_std((close / ts_delay(close, 1) - 1), 10) / (ts_std((close / ts_delay(close, 1) - 1), 30) + 1e-8))))",
        "category": "Volatility",
    },
    {
        "name": "Amount Concentration",
        "formula": "(-cs_rank((ts_max(amount, 5) / (ts_mean(amount, 20) + 1e-8))))",
        "category": "Turnover",
    },
    {
        "name": "Cross-Sectional Volume Rank",
        "formula": "(-cs_rank((vol / (ts_mean(vol, 60) + 1e-8))))",
        "category": "Volume",
    },
    {
        "name": "Gap Momentum",
        "formula": "(-cs_rank(ts_sum(((open - ts_delay(close, 1)) / (ts_delay(close, 1) + 1e-8)), 5)))",
        "category": "Overnight",
    },
    # {
    #     "name": 'VWAP Distance Decay',
    #     "formula": '# TODO: Neg(CsRank(Decay(Div(Sub($close, $vwap), Add($vwap, 1e-8)), 10)))',
    #     "category": 'VWAP',
    # },
    {
        "name": "Tail Risk Indicator",
        "formula": "(-cs_rank((ts_min((close / ts_delay(close, 1) - 1), 20) / (ts_std((close / ts_delay(close, 1) - 1), 20) + 1e-8))))",
        "category": "Risk",
    },
    {
        "name": "Volatility-Regime Reversal Divergence",
        "formula": "if_((ts_std((close / ts_delay(close, 1) - 1), 12) > ts_mean(ts_std((close / ts_delay(close, 1) - 1), 12), 48)), (-cs_rank(ts_delta(close, 3))), (-cs_rank(((close - low) / ((high - low) + 0.0001)))))",
        "category": "Regime-switching",
    },
    {
        "name": "Regime Volume Signal",
        "formula": "if_((vol > ts_mean(vol, 20)), (-cs_rank((close / ts_delay(close, 1) - 1))), (-cs_rank(ts_pct_change(close, 5))))",
        "category": "Regime-switching",
    },
    {
        "name": "Liquidity-Adjusted Reversal",
        "formula": "(-cs_rank((ts_pct_change(close, 3) * (vol / (ts_mean(vol, 20) + 1e-8)))))",
        "category": "Mean-reversion",
    },
    {
        "name": "Cross-Sectional Volatility Rank",
        "formula": "(-cs_rank(cs_rank(ts_std((close / ts_delay(close, 1) - 1), 10))))",
        "category": "Volatility",
    },
    {
        "name": "VWAP Bollinger",
        "formula": "(-cs_rank((((amount / vol) - ts_mean((amount / vol), 20)) / (ts_std((amount / vol), 20) + 1e-8))))",
        "category": "VWAP",
    },
    {
        "name": "Smoothed Return Reversal",
        "formula": "(-cs_rank(ts_ema((close / ts_delay(close, 1) - 1), 5)))",
        "category": "Mean-reversion",
    },
    {
        "name": "Volume-Price Divergence",
        "formula": "(-cs_rank((ts_rank(vol, 10) - ts_rank(close, 10))))",
        "category": "Volume",
    },
    # {
    #     "name": 'Decay Weighted Momentum',
    #     "formula": '# TODO: Neg(CsRank(Decay($returns, 20)))',
    #     "category": 'Momentum',
    # },
    {
        "name": "Range Percentile",
        "formula": "(-cs_rank(((close - ts_min(close, 20)) / ((ts_max(close, 20) - ts_min(close, 20)) + 1e-8))))",
        "category": "Mean-reversion",
    },
    {
        "name": "Volume Skewness",
        "formula": "(-cs_rank(ts_skew(vol, 20)))",
        "category": "Volume",
    },
    # {
    #     "name": 'Residual Momentum',
    #     "formula": '# TODO: Neg(CsRank(TsLinRegResid($close, 20)))',
    #     "category": 'Momentum',
    # },
    {
        "name": "VWAP Trend",
        "formula": "(-cs_rank(ts_delta(((close - (amount / vol)) / (amount / vol)), 5)))",
        "category": "VWAP",
    },
    {
        "name": "Return Autocorrelation",
        "formula": "(-cs_rank(ts_corr((close / ts_delay(close, 1) - 1), ts_delay((close / ts_delay(close, 1) - 1), 1), 10)))",
        "category": "Mean-reversion",
    },
    {
        "name": "Price Efficiency",
        "formula": "(-cs_rank((abs(ts_sum((close / ts_delay(close, 1) - 1), 10)) / (ts_sum(abs((close / ts_delay(close, 1) - 1)), 10) + 1e-8))))",
        "category": "Trend",
    },
    {
        "name": "Relative Volume Change",
        "formula": "(-cs_rank(ts_pct_change(vol, 5)))",
        "category": "Volume",
    },
    {
        "name": "Weighted VWAP Position",
        "formula": "(-cs_rank(ts_wma(((close - (amount / vol)) / (amount / vol)), 10)))",
        "category": "VWAP",
    },
    {
        "name": "Regime Momentum Flip",
        "formula": "if_((ts_mean((close / ts_delay(close, 1) - 1), 5) > 0), (-cs_rank(ts_pct_change(close, 10))), cs_rank(ts_pct_change(close, 3)))",
        "category": "Regime-switching",
    },
    {
        "name": "High-Low Volatility",
        "formula": "(-cs_rank(ts_mean(((high - low) / (close + 1e-8)), 10)))",
        "category": "Volatility",
    },
    {
        "name": "Opening Gap Reversal",
        "formula": "(-cs_rank(((open - ts_delay(close, 1)) / (ts_std((close / ts_delay(close, 1) - 1), 10) + 1e-8))))",
        "category": "Overnight",
    },
    {
        "name": "Volume Momentum Spread",
        "formula": "(-cs_rank((ts_mean(vol, 5) - ts_mean(vol, 40))))",
        "category": "Volume",
    },
    {
        "name": "Regime Volume Reversal",
        "formula": "if_(((vol / (ts_mean(vol, 20) + 1e-8)) > 1.5), (-cs_rank((close / ts_delay(close, 1) - 1))), (-cs_rank(ts_pct_change(close, 10))))",
        "category": "Regime-switching",
    },
    # {
    #     "name": 'Slope Reversal',
    #     "formula": '# TODO: Neg(CsRank(TsLinRegSlope($close, 5)))',
    #     "category": 'Mean-reversion',
    # },
    # {
    #     "name": 'VWAP Momentum Decay',
    #     "formula": '# TODO: Neg(CsRank(Decay(Return($vwap, 3), 10)))',
    #     "category": 'VWAP',
    # },
    {
        "name": "Turnover Rate Change",
        "formula": "(-cs_rank(ts_delta((amount / (vol + 1e-8)), 10)))",
        "category": "Turnover",
    },
    # {
    #     "name": 'Return Quantile Signal',
    #     "formula": '# TODO: Neg(CsRank(Quantile($returns, 20, 0.75)))',
    #     "category": 'Higher-moment',
    # },
    {
        "name": "Double EMA Crossover",
        "formula": "(-cs_rank((ts_ema(close, 5) - ts_ema(close, 20))))",
        "category": "Trend",
    },
    {
        "name": "Conditional Volatility Return",
        "formula": "(-cs_rank(((close / ts_delay(close, 1) - 1) / (ts_std((close / ts_delay(close, 1) - 1), 10) + 1e-8))))",
        "category": "Risk",
    },
    # {
    #     "name": 'Amplitude Trend',
    #     "formula": '# TODO: Neg(CsRank(TsLinRegSlope(Div(Sub($high, $low), Add($close, 1e-8)), 10)))',
    #     "category": 'Volatility',
    # },
    {
        "name": "Volume-Weighted Range",
        "formula": "(-cs_rank(ts_mean((((high - low) / (close + 1e-8)) * vol), 10)))",
        "category": "Volume",
    },
    {
        "name": "Intraday Efficiency Ratio",
        "formula": "(-cs_rank((abs((close - open)) / ((high - low) + 1e-8))))",
        "category": "Intraday",
    },
    {
        "name": "Cumulative Volume Signal",
        "formula": "(-cs_rank((ts_sum(((close / ts_delay(close, 1) - 1) * vol), 20) / (ts_sum(vol, 20) + 1e-8))))",
        "category": "Volume",
    },
    {
        "name": "VWAP Cross-Sectional Momentum",
        "formula": "(-cs_rank(cs_rank(ts_pct_change((amount / vol), 10))))",
        "category": "VWAP",
    },
    {
        "name": "Mean-Reversion Indicator",
        "formula": "(-cs_rank(((close - ts_mean(close, 10)) / (ts_mean(close, 10) + 1e-8))))",
        "category": "Mean-reversion",
    },
    {
        "name": "Volume Regime Indicator",
        "formula": "(-cs_rank((ts_std(vol, 5) / (ts_std(vol, 20) + 1e-8))))",
        "category": "Volume",
    },
    {
        "name": "Return Persistence",
        "formula": "(-cs_rank((sign(ts_delta(close, 1)) * sign(ts_delta(close, 5)))))",
        "category": "Momentum",
    },
    # {
    #     "name": 'Regime Trend Strength',
    #     "formula": '# TODO: IfElse(Greater(Abs(TsLinRegSlope($close, 20)), Std($close, 20)), Neg(CsRank(TsLinRegSlope($close, 5))), Neg(CsRank(Return($close, 3))))',
    #     "category": 'Regime-switching',
    # },
    {
        "name": "VWAP Dispersion",
        "formula": "(-cs_rank(ts_std(((close - (amount / vol)) / (amount / vol)), 10)))",
        "category": "VWAP",
    },
    {
        "name": "Smart Money Flow",
        "formula": "(-cs_rank(ts_sum((if_((close > ts_delay(close, 1)), vol, (-vol)) * ((high - low) / (close + 1e-8))), 10)))",
        "category": "Volume",
    },
    {
        "name": "Return Rank Dispersion",
        "formula": "(-cs_rank((ts_rank((close / ts_delay(close, 1) - 1), 5) - ts_rank((close / ts_delay(close, 1) - 1), 20))))",
        "category": "Mean-reversion",
    },
    {
        "name": "Volume Acceleration",
        "formula": "(-cs_rank((ts_delta(vol, 5) - ts_delta(ts_delay(vol, 5), 5))))",
        "category": "Volume",
    },
    {
        "name": "Close-Low Ratio Trend",
        "formula": "(-cs_rank(ts_mean(((close - low) / ((high - low) + 1e-8)), 5)))",
        "category": "Mean-reversion",
    },
    # {
    #     "name": 'Hull MA Deviation',
    #     "formula": '# TODO: Neg(CsRank(Div(Sub($close, HMA($close, 10)), Add(Std($close, 10), 1e-8))))',
    #     "category": 'Trend',
    # },
    # {
    #     "name": 'DEMA Momentum Signal',
    #     "formula": '# TODO: Neg(CsRank(Sub(DEMA($close, 5), DEMA($close, 20))))',
    #     "category": 'Momentum',
    # },
    {
        "name": "Volume Profile Skew",
        "formula": "(-cs_rank(ts_skew((vol / (ts_mean(vol, 20) + 1e-8)), 10)))",
        "category": "Volume",
    },
    {
        "name": "Conditional VWAP Signal",
        "formula": "if_((close > (amount / vol)), (-cs_rank(((close - (amount / vol)) / (amount / vol)))), cs_rank((((amount / vol) - close) / (amount / vol))))",
        "category": "VWAP",
    },
    {
        "name": "Extreme Volume Reversal",
        "formula": "(-cs_rank((if_((vol > (2 * ts_mean(vol, 20))), 1, 0) * (close / ts_delay(close, 1) - 1))))",
        "category": "Volume",
    },
    {
        "name": "Range Expansion Signal",
        "formula": "(-cs_rank(((high - low) / (ts_mean((high - low), 20) + 1e-8))))",
        "category": "Volatility",
    },
    {
        "name": "Short-Term IC Momentum",
        "formula": "(-cs_rank(ts_sum((sign((close / ts_delay(close, 1) - 1)) * abs((close / ts_delay(close, 1) - 1))), 5)))",
        "category": "Momentum",
    },
    {
        "name": "VWAP Curvature",
        "formula": "(-cs_rank(((((amount / vol) - ts_delay((amount / vol), 5)) / (ts_delay((amount / vol), 5) + 1e-8)) - ((ts_delay((amount / vol), 5) - ts_delay((amount / vol), 10)) / (ts_delay((amount / vol), 10) + 1e-8)))))",
        "category": "VWAP",
    },
    {
        "name": "Relative Strength",
        "formula": "(-cs_rank((ts_pct_change(close, 5) / (ts_pct_change(close, 20) + 1e-8))))",
        "category": "Momentum",
    },
    {
        "name": "Volume-Correlated Return",
        "formula": "(-cs_rank(ts_cov((close / ts_delay(close, 1) - 1), vol, 10)))",
        "category": "Volume",
    },
    {
        "name": "Regime Volatility Band",
        "formula": "if_((ts_std((close / ts_delay(close, 1) - 1), 5) > (1.5 * ts_std((close / ts_delay(close, 1) - 1), 20))), (-cs_rank(ts_pct_change(close, 1))), (-cs_rank(ts_pct_change(close, 10))))",
        "category": "Regime-switching",
    },
    {
        "name": "Open-Close Spread Momentum",
        "formula": "(-cs_rank(ts_mean(((close - open) / (open + 1e-8)), 5)))",
        "category": "Intraday",
    },
    {
        "name": "Volatility-Scaled Reversal",
        "formula": "(-cs_rank((ts_pct_change(close, 5) / (ts_std((close / ts_delay(close, 1) - 1), 20) + 1e-8))))",
        "category": "Mean-reversion",
    },
    {
        "name": "VWAP Time-Weighted Signal",
        "formula": "(-cs_rank(ts_wma(((close - (amount / vol)) / ((amount / vol) + 1e-8)), 20)))",
        "category": "VWAP",
    },
    {
        "name": "Covariance Structure Shift",
        "formula": "(-cs_rank((ts_cov((close / ts_delay(close, 1) - 1), vol, 5) - ts_cov((close / ts_delay(close, 1) - 1), vol, 20))))",
        "category": "Volume",
    },
    # {
    #     "name": 'Quadratic Regression Residual',
    #     "formula": '# TODO: Neg(CsRank(TsLinRegResid(Square($returns), 20)))',
    #     "category": 'Higher-moment',
    # },
    {
        "name": "VWAP Mean-Reversion Strength",
        "formula": "(-cs_rank((((close - (amount / vol)) / (amount / vol)) * (vol / (ts_mean(vol, 20) + 1e-8)))))",
        "category": "VWAP",
    },
    {
        "name": "Multi-Scale Momentum",
        "formula": "(-cs_rank((ts_pct_change(close, 5) + ts_pct_change(close, 20))))",
        "category": "Momentum",
    },
    {
        "name": "Relative High Position",
        "formula": "(-cs_rank(((ts_max(high, 20) - close) / (ts_max(high, 20) + 1e-8))))",
        "category": "Mean-reversion",
    },
    {
        "name": "Turnover Volatility",
        "formula": "(-cs_rank(ts_std((amount / (vol + 1e-8)), 10)))",
        "category": "Turnover",
    },
    {
        "name": "Regime Correlation Signal",
        "formula": "if_((abs(ts_corr(close, vol, 10)) > 0.5), (-cs_rank(ts_pct_change(close, 3))), (-cs_rank(ts_pct_change(close, 10))))",
        "category": "Regime-switching",
    },
    {
        "name": "Intraday Momentum Reversal",
        "formula": "(-cs_rank(((close - open) / ((high - low) + 1e-8))))",
        "category": "Intraday",
    },
    # {
    #     "name": 'Volume-Weighted Slope',
    #     "formula": '# TODO: Neg(CsRank(TsLinRegSlope(Mul($returns, $volume), 10)))',
    #     "category": 'Volume',
    # },
    {
        "name": "Adaptive Range Reversal",
        "formula": "if_((ts_std((close / ts_delay(close, 1) - 1), 10) > ts_mean(ts_std((close / ts_delay(close, 1) - 1), 10), 40)), (-cs_rank(((close - ts_min(close, 10)) / ((ts_max(close, 10) - ts_min(close, 10)) + 1e-8)))), (-cs_rank(ts_pct_change(close, 5))))",
        "category": "Regime-switching",
    },
]


def import_from_paper(
    path: Optional[Union[str, Path]] = None,
) -> FactorLibrary:
    """Import the 110 factors from the paper's Appendix P.

    If *path* is given and points to a JSON file with a ``"factors"`` list,
    those entries are loaded instead of the built-in catalog.  Each entry
    must have ``"name"``, ``"formula"``, and ``"category"`` keys.

    Parameters
    ----------
    path : str or Path, optional
        Optional JSON file to load factors from.

    Returns
    -------
    FactorLibrary
        A new library pre-populated with the paper's factors. Since no
        market data is provided, signals are ``None`` and the correlation
        matrix is not computed.
    """
    if path is not None:
        path = Path(path)
        with open(path, "r") as fp:
            raw = json.load(fp)
        entries = raw if isinstance(raw, list) else raw.get("factors", [])
    else:
        entries = PAPER_FACTORS

    library = FactorLibrary(correlation_threshold=0.5, ic_threshold=0.04)

    for i, entry in enumerate(entries):
        factor = Factor(
            id=0,  # Will be assigned by admit_factor
            name=entry["name"],
            formula=entry["formula"],
            category=entry["category"],
            ic_mean=entry.get("ic_mean", 0.0),
            icir=entry.get("icir", 0.0),
            ic_win_rate=entry.get("ic_win_rate", 0.0),
            max_correlation=entry.get("max_correlation", 0.0),
            batch_number=entry.get("batch_number", 0),
            admission_date=entry.get("admission_date", ""),
            signals=None,
        )
        if factor.formula.strip().startswith("# TODO"):
            factor.metadata["unsupported"] = True
        library.admit_factor(factor)

    logger.info(
        "Imported %d factors from %s",
        library.size,
        path if path else "built-in paper catalog",
    )
    return library
