diff --git a/quant_alpha_factor_demo.ipynb b/quant_alpha_factor_demo.ipynb new file mode 100644 index 0000000..01a839e --- /dev/null +++ b/quant_alpha_factor_demo.ipynb @@ -0,0 +1,1190 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a476d03b", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# Quantitative Trading — Alpha Factor Research Demo\n", + "# 量化交易 — Alpha 因子研究演示\n", + "\n", + "# =============================================================================\n", + "#\n", + "# Alpha 因子 (Alpha Factor) 是量化选股的核心工具。\n", + "# 它是一个数学公式,从市场数据中提取信号,预测哪些股票未来会跑赢大盘。\n", + "#\n", + "# Alpha factors are the core tool of quantitative stock selection.\n", + "# Each factor is a mathematical formula that extracts a signal from market data\n", + "# to predict which stocks will outperform in the future.\n", + "#\n", + "# 研究流程 / Research Workflow:\n", + "# §0 合成股票池 Synthetic Universe (50只股票 × 3年日线数据)\n", + "# §1 因子构建 Factor Construction (5个经典因子)\n", + "# §2 因子预处理 Factor Preprocessing (去极值、截面标准化、市值中性化)\n", + "# §3 IC 分析 IC Analysis (信息系数 / 因子预测能力量化)\n", + "# §4 分层回测 Quantile Analysis (五分位收益分层验证)\n", + "# §5 因子合成 Factor Combination (等权 & IC加权合成因子)\n", + "# §6 多空组合 Long-Short Portfolio (Top/Bottom 20% 多空策略)\n", + "# §7 因子衰减 Factor Decay (IC 随持有期延长的衰减曲线)\n", + "# §8 可视化 Visualization (9面板汇总图)\n", + "#\n", + "# 前置条件 / Prerequisites:\n", + "# pip install numpy pandas matplotlib scipy\n", + "#\n", + "# 运行方式 / Run:\n", + "# python quant_alpha_factor_demo.py\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00e42fb4", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib\n", + "matplotlib.use('Agg') # 非交互模式,避免 GUI 阻塞 / non-interactive, prevents GUI block\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.gridspec as gridspec\n", + "from matplotlib.ticker import FuncFormatter\n", + "from scipy import stats\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "# 中文字体配置 / Chinese font configuration\n", + "plt.rcParams['font.sans-serif'] = ['WenQuanYi Zen Hei', 'Arial Unicode MS', 'SimHei', 'DejaVu Sans']\n", + "plt.rcParams['axes.unicode_minus'] = False\n", + "\n", + "np.random.seed(42)\n", + "print(\"=\" * 70)\n", + "print(\" 量化交易 Alpha 因子研究演示\")\n", + "print(\" Quantitative Trading: Alpha Factor Research Demo\")\n", + "print(\"=\" * 70)" + ] + }, + { + "cell_type": "markdown", + "id": "986c9755", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# §0 合成股票池 Synthetic Stock Universe\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# 真实因子研究需要几百到几千只股票的截面数据 (cross-sectional data)。\n", + "# 我们用\"隐藏因子生成模型\" (Hidden Factor Generative Model) 合成数据,\n", + "# 确保我们构建的因子确实具有预测能力:\n", + "#\n", + "# Real factor research requires cross-sectional data of hundreds of stocks.\n", + "# We use a Hidden Factor Generative Model to synthesize data, ensuring the\n", + "# factors we build actually have genuine predictive power.\n", + "#\n", + "# 每只股票的日收益 = 市场分量 + 质量Alpha + 动量Alpha + 特质噪音\n", + "# stock_daily_return = market_component + quality_alpha + momentum_alpha + noise\n", + "#\n", + "# 其中:\n", + "# market_component = β_i × r_market (系统性风险 / systematic risk)\n", + "# quality_alpha = λ_q × quality_score_{i,t} (质量因子加载, 季度持续)\n", + "# momentum_alpha = λ_m × momentum_score_{i,t} (动量因子加载, 月度持续)\n", + "# noise ~ N(0, σ_idio) (特质风险 / idiosyncratic risk)\n", + "#\n", + "# 关键: quality_score 和 momentum_score 是\"隐藏\"的,我们无法直接观察。\n", + "# 但当我们从价格数据中计算因子时,会自然捕捉到这些隐藏信号。\n", + "# Key: these hidden scores are unobservable, but our computed factors will\n", + "# naturally capture them — this is exactly what alpha factors do!\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1332377", + "metadata": {}, + "outputs": [], + "source": [ + "N_STOCKS = 50 # 股票数量 / number of stocks\n", + "N_DAYS = 756 # 交易日 ≈ 3 年 / trading days ≈ 3 years\n", + "N_QUARTERS = N_DAYS // 63 + 2 # 季度数 / number of quarters\n", + "N_MONTHS = N_DAYS // 21 + 2 # 月度数 / number of months\n", + "\n", + "# 交易日期序列 / Business-day date index\n", + "dates = pd.bdate_range(start=\"2021-01-04\", periods=N_DAYS)\n", + "\n", + "# 股票代码 (模拟A股格式) / Stock symbols (simulated A-share format)\n", + "symbols = [f\"{str(i).zfill(6)}.{'SH' if i % 2 == 0 else 'SZ'}\" for i in range(1, N_STOCKS + 1)]\n", + "\n", + "# ── 0-A 隐藏质量因子(季度持续)/ Hidden Quality Factor (Quarterly Persistence) ──\n", + "#\n", + "# 质量因子 AR(1): quality_t = 0.90 × quality_{t-1} + shock\n", + "# 季度自回归系数 0.90 → 年度持续性 ≈ 0.90^4 = 0.66(持续性更强)\n", + "# Quarterly AR(1) = 0.90 → annual persistence ≈ 0.90^4 = 0.66 (more persistent)\n", + "quality_quarterly = np.zeros((N_STOCKS, N_QUARTERS))\n", + "quality_quarterly[:, 0] = np.random.randn(N_STOCKS)\n", + "for q in range(1, N_QUARTERS):\n", + " # sqrt(1 - 0.90^2) ≈ 0.436 保持方差为 1 / keeps variance = 1\n", + " quality_quarterly[:, q] = (0.90 * quality_quarterly[:, q - 1]\n", + " + 0.436 * np.random.randn(N_STOCKS))\n", + "\n", + "# 季度 → 每日映射 / Map quarterly to daily\n", + "quality_daily = np.zeros((N_DAYS, N_STOCKS))\n", + "for d in range(N_DAYS):\n", + " q_idx = min(d // 63, N_QUARTERS - 1)\n", + " quality_daily[d] = quality_quarterly[:, q_idx]\n", + "\n", + "# ── 0-B 隐藏动量因子(月度持续)/ Hidden Momentum Factor (Monthly Persistence) ──\n", + "#\n", + "# 动量因子 AR(1): momentum_t = 0.60 × momentum_{t-1} + shock\n", + "# 月度自回归系数 0.60,意味着 3 个月后的持续性 ≈ 0.60^3 = 0.22\n", + "# Monthly AR(1) coefficient 0.60 → 3-month persistence ≈ 0.22\n", + "momentum_monthly = np.zeros((N_STOCKS, N_MONTHS))\n", + "momentum_monthly[:, 0] = np.random.randn(N_STOCKS)\n", + "for m in range(1, N_MONTHS):\n", + " # 月度 AR(1) = 0.80,年度持续性 ≈ 0.80^12 = 0.069(合理的动量衰减)\n", + " # Monthly AR(1) = 0.80 → annual persistence ≈ 0.80^12 = 0.069\n", + " momentum_monthly[:, m] = (0.80 * momentum_monthly[:, m - 1]\n", + " + 0.60 * np.random.randn(N_STOCKS))\n", + "\n", + "momentum_daily = np.zeros((N_DAYS, N_STOCKS))\n", + "for d in range(N_DAYS):\n", + " m_idx = min(d // 21, N_MONTHS - 1)\n", + " momentum_daily[d] = momentum_monthly[:, m_idx]\n", + "\n", + "# ── 0-C 日收益率生成 / Daily Return Generation ────────────────────────────────\n", + "#\n", + "# 日市场收益 (Market Daily Return): 年化μ=8%, σ=20%\n", + "mkt_returns = np.random.normal(0.0003, 0.0126, N_DAYS) # 0.20/√252 ≈ 0.0126\n", + "\n", + "# 每只股票的市场贝塔 β: 均匀分布在 [0.5, 1.5]\n", + "# Each stock's market beta β: uniformly distributed in [0.5, 1.5]\n", + "stock_betas = np.random.uniform(0.5, 1.5, N_STOCKS)\n", + "\n", + "# (N_DAYS, N_STOCKS): 每列是一只股票,每行是一个交易日\n", + "market_component = mkt_returns[:, np.newaxis] * stock_betas[np.newaxis, :] # 市场分量\n", + "quality_component = 0.00080 * quality_daily # 质量贡献:约 ±20% 年化 alpha\n", + "momentum_component = 0.00050 * momentum_daily # 动量贡献:约 ±12% 年化 alpha\n", + "idio_noise = np.random.normal(0, 0.0075, (N_DAYS, N_STOCKS)) # 特质噪音\n", + "\n", + "# 日对数收益率 / Daily log returns\n", + "log_returns = market_component + quality_component + momentum_component + idio_noise\n", + "\n", + "# ── 0-D 价格路径 / Price Paths ────────────────────────────────────────────────\n", + "# P_t = P_0 × exp( Σ log_return_s, s=1..t ) (价格路径 / price path)\n", + "initial_prices = np.random.uniform(5.0, 100.0, N_STOCKS) # A股初始价格\n", + "prices_arr = initial_prices * np.exp(np.cumsum(log_returns, axis=0))\n", + "\n", + "prices_df = pd.DataFrame(prices_arr, index=dates, columns=symbols)\n", + "log_ret_df = pd.DataFrame(log_returns, index=dates, columns=symbols)\n", + "simple_ret_df = prices_df.pct_change() # 简单收益率: r_t = P_t/P_{t-1} - 1\n", + "\n", + "# ── 0-E 成交量生成 / Volume Generation ──────────────────────────────────────\n", + "# 真实市场中,成交量与绝对收益率正相关(大涨大跌时换手更活跃)。\n", + "# In real markets, volume correlates with |return| (more trading on big moves).\n", + "#\n", + "# log(volume) = log(base) + 3×|log_return| + noise\n", + "base_volumes = np.random.uniform(1e6, 5e7, N_STOCKS) # 基础日均成交量(股)\n", + "vol_multiplier = np.exp(3.0 * np.abs(log_returns) + 0.3 * np.random.randn(N_DAYS, N_STOCKS))\n", + "volume_df = pd.DataFrame(base_volumes * vol_multiplier, index=dates, columns=symbols)\n", + "\n", + "# ── 0-F 模拟市值 / Simulated Market Capitalization ───────────────────────────\n", + "# 市值 (Market Cap) = 价格 × 流通股数 / Market Cap = Price × Float Shares\n", + "float_shares = np.random.uniform(1e8, 1e10, N_STOCKS) # 流通股数(股)\n", + "mktcap_df = prices_df * float_shares # 市值(元)\n", + "log_mktcap_df = np.log(mktcap_df) # 对数市值(因子分析常用)\n", + "\n", + "market_series = pd.Series(mkt_returns, index=dates) # 市场收益率序列\n", + "\n", + "print(f\"\\n[§0] 股票池生成完成 / Universe Generated\")\n", + "print(f\" 股票数量 (Stocks): {N_STOCKS} 只\")\n", + "print(f\" 交易日数 (Days): {N_DAYS} 天 {dates[0].date()} ~ {dates[-1].date()}\")\n", + "print(f\" 价格区间 (Price): {prices_df.min().min():.2f} ~ {prices_df.max().max():.2f} 元\")" + ] + }, + { + "cell_type": "markdown", + "id": "04c44408", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# §1 因子构建 Factor Construction\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# 我们构建 5 个来自学术文献的经典因子,涵盖不同的 Alpha 来源:\n", + "# We build 5 classic factors from academic literature, covering different alpha sources:\n", + "#\n", + "# ┌──────┬────────────────────────────┬──────────────────────────────────────────┐\n", + "# │ 因子 │ 名称 │ 公式 & 来源 │\n", + "# ├──────┼────────────────────────────┼──────────────────────────────────────────┤\n", + "# │ MOM │ 动量 Momentum │ r(t-252, t-21) Jegadeesh & Titman 1993 │\n", + "# │ REV │ 短期反转 Reversal │ -r(t-21, t) Jegadeesh 1990 │\n", + "# │ LVOL │ 低波动 Low Volatility │ -std(r, 60D) Baker et al. 2011 │\n", + "# │ BAB │ 低贝塔 Betting vs Beta │ -β(60D) Frazzini & Pedersen 2014 │\n", + "# │ ILLIQ│ 非流动性 Amihud Illiquidity│ mean(|r|/V,20D) Amihud 2002 │\n", + "# └──────┴────────────────────────────┴──────────────────────────────────────────┘\n", + "#\n", + "# 重要约定: 所有因子都以\"因子值越高 → 预期未来收益越高\"为正方向。\n", + "# Convention: higher factor value → expected higher future return (unified sign).\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d852ac0", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n[§1] 构建因子 / Constructing factors...\")\n", + "\n", + "# ── 1-A 动量因子 (MOM) Momentum Factor ──────────────────────────────────────\n", + "#\n", + "# 来源 / Source: Jegadeesh & Titman (1993)\n", + "# \"Returns to Buying Winners and Selling Losers\"\n", + "#\n", + "# 动量效应 (Momentum Effect): 过去 12 个月(跳过最近 1 个月)涨幅最大的股票,\n", + "# 未来 3-12 个月通常继续跑赢市场。这是学术上记录最多的股票市场异象之一。\n", + "#\n", + "# Momentum effect: stocks with highest 12M-1M returns tend to outperform\n", + "# over the next 3-12 months. One of the most documented stock market anomalies.\n", + "#\n", + "# 为什么要跳过最近 1 个月?/ Why skip the last month?\n", + "# 最近 1 个月的收益存在短期反转效应(买卖价差、做市商库存调整等微观结构原因),\n", + "# 会污染中期动量信号,所以必须跳过。\n", + "# The last month exhibits short-term reversal due to bid-ask spread and\n", + "# market-maker inventory rebalancing, contaminating medium-term momentum.\n", + "#\n", + "# 公式 / Formula:\n", + "# MOM_t = (P_{t-21} / P_{t-252}) - 1 (从12M前 到 1M前 的累积收益)\n", + "# MOM_t = (P_{t-21} / P_{t-252}) - 1 (cumulative return from 12M to 1M ago)\n", + "\n", + "MOM_LONG = 252 # 长回看窗口: 12个月 / lookback long: 12 months\n", + "MOM_SHORT = 21 # 跳过窗口: 1个月 / skip window: 1 month\n", + "\n", + "factor_mom = prices_df.shift(MOM_SHORT) / prices_df.shift(MOM_LONG) - 1.0\n", + "print(f\" ✓ MOM 动量因子: 有效值 {factor_mom.notna().mean().mean():.1%}\")\n", + "\n", + "# ── 1-B 短期反转因子 (REV) Short-Term Reversal Factor ───────────────────────\n", + "#\n", + "# 来源 / Source: Jegadeesh (1990)\n", + "# \"Evidence of Predictable Behavior of Security Returns\"\n", + "#\n", + "# 反转效应 (Reversal Effect): 上个月大涨的股票,下个月往往小幅回调;\n", + "# 大跌的股票往往小幅反弹。这是对短期\"过度反应\"的修正。\n", + "#\n", + "# Reversal effect: last month's winners tend to reverse; losers tend to bounce.\n", + "# This is correction of short-term overreaction.\n", + "#\n", + "# 注意: 取负号是因为月涨 → 因子值高 → 预期下跌(反转),我们统一正方向。\n", + "# Note: negative sign because high past return → expected to reverse → lower future return,\n", + "# we flip to maintain convention: higher factor → higher expected return.\n", + "#\n", + "# 公式 / Formula:\n", + "# REV_t = -(P_t / P_{t-21} - 1) (负1月收益 / negative 1-month return)\n", + "\n", + "factor_rev = -(prices_df / prices_df.shift(MOM_SHORT) - 1.0)\n", + "print(f\" ✓ REV 反转因子: 有效值 {factor_rev.notna().mean().mean():.1%}\")\n", + "\n", + "# ── 1-C 低波动因子 (LVOL) Low Volatility Factor ────────────────────────────\n", + "#\n", + "# 来源 / Source: Ang et al.(2006); Baker, Bradley & Wurgler (2011)\n", + "# \"Benchmarks as Limits to Arbitrage: Understanding the Low-Volatility Anomaly\"\n", + "#\n", + "# 低波动异象 (Low Volatility Anomaly): 经典金融理论 CAPM 预测高风险 = 高收益。\n", + "# 但实证研究发现,低波动率的股票反而有更高的原始收益和风险调整后收益!\n", + "#\n", + "# Low-vol anomaly: contrary to CAPM, low-volatility stocks earn HIGHER\n", + "# risk-adjusted and even raw returns than high-volatility stocks.\n", + "#\n", + "# 可能的解释 / Possible explanations:\n", + "# ① 机构投资者受基准约束,被迫持有高贝塔股票 (benchmark constraints)\n", + "# ② 投资者对\"彩票式\"高波动股票有偏好,导致其被高估 (lottery preference)\n", + "# ③ 杠杆限制阻止套利者纠正定价偏差 (leverage constraints)\n", + "#\n", + "# 公式 / Formula:\n", + "# LVOL_t = -σ(r_{t-60:t}) (负60日已实现波动率 / negative 60-day realized vol)\n", + "\n", + "VOL_WINDOW = 60 # 60 个交易日 ≈ 3 个月 / 60 trading days ≈ 3 months\n", + "factor_lvol = -(simple_ret_df.rolling(VOL_WINDOW).std())\n", + "print(f\" ✓ LVOL 低波动因子: 有效值 {factor_lvol.notna().mean().mean():.1%}\")\n", + "\n", + "# ── 1-D 低贝塔因子 (BAB) Betting Against Beta ──────────────────────────────\n", + "#\n", + "# 来源 / Source: Frazzini & Pedersen (2014) \"Betting Against Beta\"\n", + "#\n", + "# BAB 异象: 做多低贝塔股票、做空高贝塔股票,可以获得持续的超额收益。\n", + "# 与低波动因子类似,但专注于对\"市场系统性风险\"的暴露,而非总波动率。\n", + "#\n", + "# BAB anomaly: long low-beta, short high-beta → persistent excess return.\n", + "# Similar to low-vol but focuses on market systematic risk exposure.\n", + "#\n", + "# 贝塔计算 / Beta computation:\n", + "# β_i = Cov(r_i, r_market) / Var(r_market) (60日滚动窗口 / 60-day rolling)\n", + "#\n", + "# 向量化技巧: 利用 pandas rolling 方法计算所有股票的滚动协方差和市场方差\n", + "# Vectorized trick: use pandas rolling to compute rolling covariance and variance\n", + "\n", + "BETA_WINDOW = 60 # 60日滚动贝塔 / 60-day rolling beta\n", + "\n", + "# pandas rolling().cov(other) 计算每个时间点的滚动协方差\n", + "# pandas rolling().cov(other) computes rolling covariance at each time point\n", + "rolling_var_mkt = market_series.rolling(BETA_WINDOW).var() # 市场方差 / market variance\n", + "rolling_cov = log_ret_df.apply(\n", + " lambda col: col.rolling(BETA_WINDOW).cov(market_series) # 逐列协方差 / per-stock cov\n", + ")\n", + "rolling_beta_df = rolling_cov.div(rolling_var_mkt, axis=0) # β = cov / var\n", + "factor_bab = -rolling_beta_df # 取负:低贝塔 → 高因子值 / flip: low-beta → high score\n", + "print(f\" ✓ BAB 低贝塔因子: 有效值 {factor_bab.notna().mean().mean():.1%}\")\n", + "\n", + "# ── 1-E Amihud 非流动性因子 (ILLIQ) Amihud Illiquidity Factor ───────────────\n", + "#\n", + "# 来源 / Source: Amihud (2002) \"Illiquidity and Stock Returns\"\n", + "#\n", + "# Amihud 非流动性指标 (Amihud Illiquidity Measure):\n", + "# ILLIQ_{i,t} = (1/D) × Σ_{d=t-D+1}^{t} |r_{i,d}| / Volume_{i,d}\n", + "#\n", + "# 含义 (Interpretation):\n", + "# \"每单位成交量能引起多大的价格变动?\"\n", + "# \"How much price movement per unit of trading volume?\"\n", + "#\n", + "# 值越高 → 流动性越差 (higher → less liquid)\n", + "# 流动性差的股票风险更高,投资者要求额外的\"流动性溢价\"(liquidity premium)\n", + "# Illiquid stocks carry higher risk, investors demand extra liquidity premium\n", + "#\n", + "# 注: 本因子中,高 ILLIQ(流动性差)→ 预期高收益(溢价补偿)→ 符合正方向约定\n", + "# Note: high ILLIQ (illiquid) → expected high return (premium) → matches positive convention\n", + "\n", + "ILLIQ_WINDOW = 20 # 20日均值 / 20-day average\n", + "\n", + "# |日收益率| / 日成交量,再取20日滚动均值\n", + "# |daily return| / daily volume, then 20-day rolling mean\n", + "price_impact = simple_ret_df.abs() / volume_df # 每日价格冲击 / daily price impact\n", + "factor_illiq = price_impact.rolling(ILLIQ_WINDOW).mean() * 1e8 # 缩放 / scaling\n", + "print(f\" ✓ ILLIQ 非流动性因子: 有效值 {factor_illiq.notna().mean().mean():.1%}\")\n", + "\n", + "# ── 1-F 汇总 / Consolidate ───────────────────────────────────────────────────\n", + "FACTORS = {\n", + " \"MOM\" : factor_mom, # 动量\n", + " \"REV\" : factor_rev, # 短期反转\n", + " \"LVOL\" : factor_lvol, # 低波动\n", + " \"BAB\" : factor_bab, # 低贝塔 / 贝塔\n", + " \"ILLIQ\" : factor_illiq, # Amihud 非流动性\n", + "}\n", + "print(f\"\\n 共构建 {len(FACTORS)} 个因子: {list(FACTORS.keys())}\")" + ] + }, + { + "cell_type": "markdown", + "id": "ab363340", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# §2 因子预处理 Factor Preprocessing\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# 原始因子值不能直接用于分析,需要经过三步标准化预处理流程:\n", + "# Raw factor values must go through a 3-step preprocessing pipeline:\n", + "#\n", + "# Step 1 ── 截面去极值 (Cross-Sectional Winsorization)\n", + "# 在每个日期截面,将超出 [μ - 3σ, μ + 3σ] 范围的值截断。\n", + "# At each date, clip values outside [mean - 3σ, mean + 3σ].\n", + "# 为什么?极端异常值会主导相关系数计算,掩盖真实的因子信号。\n", + "# Why? Outliers dominate correlation calculations and mask the true signal.\n", + "#\n", + "# Step 2 ── 截面 Z-score 标准化 (Cross-Sectional Z-score)\n", + "# z_{i,t} = (x_{i,t} - μ_t) / σ_t (在每个时间截面 t 上计算)\n", + "# z_{i,t} = (x_{i,t} - μ_t) / σ_t (computed cross-sectionally at each date t)\n", + "# 为什么?不同因子量纲不同(动量是%,波动率也是%,ILLIQ 是极小数),\n", + "# 标准化后可以直接比较和合成。\n", + "# Why? Factors have different scales; Z-score enables fair comparison and combination.\n", + "#\n", + "# Step 3 ── 市值中性化 (Market Cap Neutralization)\n", + "# 在每个截面,对 log(市值) 做 OLS 回归,用残差替换原因子值:\n", + "# At each date, regress on log(mktcap) via OLS; use residuals as factor:\n", + "# factor_neutral_i = factor_i - (α + β × log_mktcap_i)\n", + "# 为什么?小市值效应 (Size Effect) 会污染其他因子。例如小市值股票往往同时具有\n", + "# 高动量、高波动率、低流动性,如果不剔除市值效应,因子实际上只是\n", + "# 在选小市值股票,而不是真正的动量/波动/流动性信号。\n", + "# Why? Size effect contaminates other factors — small caps often have high momentum,\n", + "# high vol, and low liquidity simultaneously. Without neutralization,\n", + "# factors simply pick small caps rather than true alpha signals.\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0fb3a541", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n[§2] 因子预处理 / Factor Preprocessing...\")\n", + "\n", + "\n", + "def winsorize_cross_section(factor_df: pd.DataFrame, n_std: float = 3.0) -> pd.DataFrame:\n", + " \"\"\"\n", + " 截面去极值 / Cross-sectional winsorization.\n", + "\n", + " 在每个日期(行),将超出 n_std 个标准差的值截断到边界。\n", + " At each date (row), clip values that are more than n_std std devs from the mean.\n", + "\n", + " 向量化实现,逐行 clip / Vectorized via per-row clip applied with apply.\n", + " \"\"\"\n", + " mu = factor_df.mean(axis=1) # 每日截面均值 / daily cross-sectional mean\n", + " sig = factor_df.std(axis=1) # 每日截面标准差 / daily cross-sectional std\n", + " lower = (mu - n_std * sig).values[:, np.newaxis] # 广播形状 / broadcast shape\n", + " upper = (mu + n_std * sig).values[:, np.newaxis]\n", + " clipped = np.clip(factor_df.values, lower, upper)\n", + " return pd.DataFrame(clipped, index=factor_df.index, columns=factor_df.columns)\n", + "\n", + "\n", + "def zscore_cross_section(factor_df: pd.DataFrame) -> pd.DataFrame:\n", + " \"\"\"\n", + " 截面 Z-score 标准化 / Cross-sectional Z-score normalization.\n", + "\n", + " 使每个时间截面的因子均值=0, 标准差=1。\n", + " Makes each time cross-section have mean=0 and std=1.\n", + " \"\"\"\n", + " mu = factor_df.mean(axis=1) # 每日截面均值 (N_DAYS,)\n", + " sig = factor_df.std(axis=1) # 每日截面标准差 (N_DAYS,)\n", + " # sub/div 沿行方向广播 / broadcast along rows\n", + " return factor_df.sub(mu, axis=0).div(sig.replace(0, np.nan), axis=0)\n", + "\n", + "\n", + "def neutralize_mktcap(factor_df: pd.DataFrame, log_mktcap: pd.DataFrame) -> pd.DataFrame:\n", + " \"\"\"\n", + " 市值中性化 / Market cap neutralization via cross-sectional OLS.\n", + "\n", + " 在每个截面,用 OLS 将因子对 log(市值) 回归,取残差作为中性化因子。\n", + " At each cross-section, regress factor on log(mktcap) via OLS; keep residuals.\n", + "\n", + " residual_i = factor_i - (intercept + slope × log_mktcap_i)\n", + " \"\"\"\n", + " # 对齐列顺序 / Align columns\n", + " y_arr = factor_df.values.copy() # (N_DAYS, N_STOCKS)\n", + " x_arr = log_mktcap.reindex(columns=factor_df.columns).values # (N_DAYS, N_STOCKS)\n", + "\n", + " for t in range(len(factor_df)):\n", + " y = y_arr[t]\n", + " x = x_arr[t]\n", + " mask = ~(np.isnan(y) | np.isnan(x))\n", + " if mask.sum() < 10:\n", + " continue\n", + " y_c, x_c = y[mask], x[mask]\n", + " # 手动 OLS / manual OLS (faster than scipy on small arrays)\n", + " xm, ym = x_c.mean(), y_c.mean()\n", + " denom = np.dot(x_c - xm, x_c - xm)\n", + " if denom < 1e-12:\n", + " continue\n", + " slope = np.dot(x_c - xm, y_c - ym) / denom\n", + " intercept = ym - slope * xm\n", + " y_arr[t][mask] = y_c - (intercept + slope * x_c) # 残差 / residuals\n", + "\n", + " return pd.DataFrame(y_arr, index=factor_df.index, columns=factor_df.columns)\n", + "\n", + "\n", + "# 对所有因子应用预处理流水线 / Apply preprocessing pipeline to all factors\n", + "FACTORS_PROCESSED = {}\n", + "for name, factor in FACTORS.items():\n", + " step1 = winsorize_cross_section(factor) # ① 去极值\n", + " step2 = zscore_cross_section(step1) # ② Z-score\n", + " step3 = neutralize_mktcap(step2, log_mktcap_df) # ③ 市值中性化\n", + " FACTORS_PROCESSED[name] = step3\n", + " print(f\" ✓ {name:5s}: 去极值 → Z-score → 市值中性化 完成\")\n", + "\n", + "print(\" 预处理完成 / Preprocessing complete.\")" + ] + }, + { + "cell_type": "markdown", + "id": "5826aca6", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# §3 IC 分析 Information Coefficient Analysis\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# IC (Information Coefficient 信息系数) 是衡量因子预测能力的黄金标准。\n", + "# IC is the gold standard for measuring a factor's predictive power.\n", + "#\n", + "# 定义 / Definition:\n", + "# IC_t = Spearman_Rank_Correlation( factor_{t}, forward_return_{t+H} )\n", + "#\n", + "# 其中:\n", + "# factor_{t} = 第 t 日截面因子值(50只股票,每只一个数)\n", + "# forward_return_{t+H} = 从第 t 日起持有 H 日的未来收益(未来数据!)\n", + "# H = 21 交易日 ≈ 1 个月(最常用的持有期)\n", + "#\n", + "# 用 Spearman 秩相关而非 Pearson 线性相关的原因:\n", + "# ① 对极端值鲁棒 (robust to outliers)\n", + "# ② 衡量单调关系(不需要线性)(measures monotonic, not necessarily linear, relationship)\n", + "#\n", + "# 为什么用 Spearman rank correlation instead of Pearson?\n", + "# ① Robust to outliers\n", + "# ② Measures monotonic relationship (no linearity assumption needed)\n", + "#\n", + "# IC 评价标准 / IC benchmark:\n", + "# |IC均值| > 0.05 → 因子有效 / factor is effective\n", + "# |IC均值| > 0.10 → 因子强 / factor is strong\n", + "# ICIR > 0.50 → 因子稳定 / factor is stable\n", + "# IC>0 比率 > 55% → 方向一致性好 / directionally consistent\n", + "#\n", + "# ICIR (IC Information Ratio):\n", + "# ICIR = IC均值 / IC标准差 = IC_mean / IC_std\n", + "# 类似夏普比率,衡量\"每单位波动贡献多少稳定的预测能力\"\n", + "# Similar to Sharpe ratio — measures how much stable predictive power per unit of volatility\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2201ba1a", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n[§3] IC 分析 / IC Analysis...\")\n", + "\n", + "FORWARD_PERIOD = 21 # 持有期 H = 21 日 ≈ 1 个月 / holding period ≈ 1 month\n", + "\n", + "\n", + "def compute_ic_series(\n", + " factor_df: pd.DataFrame,\n", + " forward_ret_df: pd.DataFrame,\n", + " holding_period: int = 21,\n", + ") -> pd.Series:\n", + " \"\"\"\n", + " 计算因子的 IC 时间序列。\n", + " Compute the IC time series for a factor.\n", + "\n", + " 在每个日期 t,计算截面 Spearman 相关系数:\n", + " At each date t, compute cross-sectional Spearman rank correlation:\n", + " IC_t = Spearmanr( factor[t, all_stocks], forward_return[t+H, all_stocks] )\n", + "\n", + " Parameters:\n", + " factor_df : 预处理后的因子值 (date × stock)\n", + " forward_ret_df : 收益率 DataFrame (date × stock)\n", + " holding_period : 持有期(交易日)/ holding period in days\n", + " Returns:\n", + " ic_series : IC 值时间序列 / IC time series indexed by date\n", + " \"\"\"\n", + " # shift(-H): 把 t+H 的收益值移到第 t 行,使得 fwd_returns.loc[t] = return from t to t+H\n", + " # shift(-H) aligns so that fwd_returns.loc[t] = the return earned from t to t+H\n", + " fwd_returns = forward_ret_df.shift(-holding_period)\n", + "\n", + " ic_values, ic_dates = [], []\n", + " for date in factor_df.index[:-holding_period]: # 最后 H 行无未来收益 / no future return\n", + " f_row = factor_df.loc[date].dropna()\n", + " r_row = fwd_returns.loc[date, f_row.index].dropna()\n", + " common = f_row.index.intersection(r_row.index)\n", + " if len(common) < 10: # 至少 10 只股票 / need at least 10 stocks\n", + " continue\n", + " ic, _ = stats.spearmanr(f_row[common].values, r_row[common].values)\n", + " ic_values.append(ic)\n", + " ic_dates.append(date)\n", + "\n", + " return pd.Series(ic_values, index=ic_dates, name=\"IC\")\n", + "\n", + "\n", + "# 逐因子计算 IC 序列 / Compute IC series for each factor\n", + "ic_series_dict = {}\n", + "for name, factor in FACTORS_PROCESSED.items():\n", + " ic_series_dict[name] = compute_ic_series(factor, simple_ret_df, FORWARD_PERIOD)\n", + "\n", + "# ── IC 汇总统计表 / IC Summary Statistics Table ───────────────────────────────\n", + "print(f\"\\n {'因子':8s} {'IC均值':>8s} {'IC标准差':>9s} {'ICIR':>8s} {'IC>0比率':>9s} 评级\")\n", + "print(f\" \" + \"─\" * 64)\n", + "\n", + "ic_stats = {}\n", + "for name, ic_s in ic_series_dict.items():\n", + " ic_mean = ic_s.mean()\n", + " ic_std = ic_s.std()\n", + " icir = ic_mean / ic_std if ic_std > 0 else 0.0\n", + " positive_rate = (ic_s > 0).mean()\n", + " ic_stats[name] = {\n", + " \"IC Mean\" : ic_mean,\n", + " \"IC Std\" : ic_std,\n", + " \"ICIR\" : icir,\n", + " \"Positive Rate\": positive_rate,\n", + " }\n", + " # 评级: ★★=强, ★=有效, ○=弱 / rating\n", + " if abs(ic_mean) > 0.08:\n", + " rating = \"★★ 强 Strong\"\n", + " elif abs(ic_mean) > 0.04:\n", + " rating = \"★ 有效 Effective\"\n", + " else:\n", + " rating = \"○ 弱 Weak\"\n", + " print(f\" {name:8s} {ic_mean:+8.4f} {ic_std:9.4f} {icir:+8.4f} {positive_rate:9.1%} {rating}\")\n", + "\n", + "print(f\" \" + \"─\" * 64)\n", + "print(f\" 评级标准: |IC均值|>0.08 ★★强 |IC均值|>0.04 ★有效 ICIR>0.5 稳定\")" + ] + }, + { + "cell_type": "markdown", + "id": "0b099a1a", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# §4 分层回测 Quantile Return Analysis\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# 分层回测 (Quantile Analysis / Bucket Test) 是验证因子有效性的经典直观方法。\n", + "# Quantile analysis is the classic, intuitive way to validate a factor.\n", + "#\n", + "# 操作步骤 / Procedure:\n", + "# ① 在每个调仓日 T,按因子值将全部股票从小到大排序\n", + "# ② 等分为 Q 组(常用 Q=5 五分位)\n", + "# ③ 每组各构建等权组合 (equal-weight portfolio),持有到下次调仓日 T+21\n", + "# ④ 记录每组收益,重复直到回测结束\n", + "#\n", + "# ① On each rebalance date T, rank all stocks by factor value\n", + "# ② Split into Q equal-sized buckets (usually Q=5 quintiles)\n", + "# ③ Each bucket forms an equal-weight portfolio, held until next rebalance T+21\n", + "# ④ Record each bucket's return, repeat until end of backtest\n", + "#\n", + "# 期望结果 / Expected result:\n", + "# Q5 累积收益 > Q4 > Q3 > Q2 > Q1\n", + "# 即因子值越高,未来收益越高 —— 这是因子有效的最直观证明\n", + "# Higher factor value → higher future return = most intuitive proof of efficacy\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a7c15a4c", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n[§4] 分层回测 / Quantile Return Analysis...\")\n", + "\n", + "N_QUANTILES = 5 # 五分位 / quintiles\n", + "REBAL_PERIOD = 21 # 月度调仓 / monthly rebalancing\n", + "\n", + "# 选用 ICIR 最高的因子做分层演示 / Use factor with highest |ICIR| for demo\n", + "best_factor_name = max(ic_stats, key=lambda n: abs(ic_stats[n][\"ICIR\"]))\n", + "best_factor = FACTORS_PROCESSED[best_factor_name]\n", + "print(f\" 最强因子 (Best Factor): {best_factor_name} ICIR={ic_stats[best_factor_name]['ICIR']:+.3f}\")\n", + "\n", + "\n", + "def compute_quantile_returns(\n", + " factor_df: pd.DataFrame,\n", + " price_df: pd.DataFrame,\n", + " n_quantiles: int = 5,\n", + " rebal_period: int = 21,\n", + ") -> pd.DataFrame:\n", + " \"\"\"\n", + " 分层回测:每月调仓,计算各五分位等权组合的期间收益。\n", + " Quintile backtest: monthly rebalancing, equal-weight portfolio returns per bucket.\n", + "\n", + " Returns:\n", + " DataFrame, columns=[Q1..Q5], index=rebalance dates,\n", + " values=equal-weight return for that holding period\n", + " \"\"\"\n", + " # 调仓日序列 / Rebalance date sequence\n", + " rebal_dates = price_df.index[::rebal_period]\n", + "\n", + " bucket_returns = {f\"Q{q}\": [] for q in range(1, n_quantiles + 1)}\n", + " period_dates = []\n", + "\n", + " for i in range(len(rebal_dates) - 1):\n", + " t0 = rebal_dates[i] # 建仓日 / entry date\n", + " t1 = rebal_dates[i + 1] # 平仓日 / exit date\n", + "\n", + " f_today = factor_df.loc[t0].dropna()\n", + " if len(f_today) < n_quantiles * 3: # 股票太少则跳过 / skip if too few stocks\n", + " continue\n", + "\n", + " # pd.qcut 按因子值等分为 Q 组 / split into Q equal-sized bins\n", + " labels = pd.qcut(\n", + " f_today.rank(method='first'), # 先按秩排名(避免重复值问题)\n", + " n_quantiles,\n", + " labels=[f\"Q{q}\" for q in range(1, n_quantiles + 1)]\n", + " )\n", + "\n", + " # 每组的持有期收益(等权平均)/ equal-weight return per group\n", + " p0 = price_df.loc[t0]\n", + " p1 = price_df.loc[t1]\n", + " hold_ret = p1 / p0 - 1.0\n", + "\n", + " for q in range(1, n_quantiles + 1):\n", + " stocks_in_q = labels[labels == f\"Q{q}\"].index\n", + " bucket_returns[f\"Q{q}\"].append(hold_ret[stocks_in_q].mean())\n", + "\n", + " period_dates.append(t0)\n", + "\n", + " return pd.DataFrame(bucket_returns, index=period_dates)\n", + "\n", + "\n", + "quantile_rets = compute_quantile_returns(best_factor, prices_df, N_QUANTILES, REBAL_PERIOD)\n", + "quantile_cumret = (1 + quantile_rets).cumprod() # 累积净值 / cumulative NAV\n", + "\n", + "# 多空价差 (Long-Short Spread): Q5(多头)- Q1(空头)\n", + "ls_spread = quantile_rets[\"Q5\"] - quantile_rets[\"Q1\"]\n", + "ls_cumret = (1 + ls_spread).cumprod()\n", + "\n", + "periods_per_year = 252.0 / REBAL_PERIOD\n", + "\n", + "print(f\"\\n {'分组':8s} {'年化收益':>10s} {'累积收益':>10s}\")\n", + "print(f\" \" + \"─\" * 40)\n", + "for q in range(1, N_QUANTILES + 1):\n", + " ret_s = quantile_rets[f\"Q{q}\"]\n", + " ann_ret = (1 + ret_s.mean()) ** periods_per_year - 1\n", + " cum_ret = quantile_cumret[f\"Q{q}\"].iloc[-1] - 1\n", + " print(f\" Q{q} {ann_ret:+10.2%} {cum_ret:+10.2%}\")\n", + "ann_ls = (1 + ls_spread.mean()) ** periods_per_year - 1\n", + "print(f\" L-S(Q5-Q1) {ann_ls:+9.2%} {ls_cumret.iloc[-1]-1:+10.2%}\")\n", + "print(f\" \" + \"─\" * 40)" + ] + }, + { + "cell_type": "markdown", + "id": "cd68d8d8", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# §5 因子合成 Factor Combination\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# 单个因子的预测能力有限(IC 通常只有 0.05~0.10),因子合成可以:\n", + "# Individual factors have limited predictive power (IC ≈ 0.05~0.10).\n", + "# Factor combination can:\n", + "#\n", + "# ① 提升综合 ICIR(多个信号互补,减少因子特定噪音)\n", + "# Improve composite ICIR (signals complement each other, reduce factor noise)\n", + "# ② 覆盖更多 Alpha 来源(动量+质量+流动性的综合)\n", + "# Cover more alpha sources (momentum + quality + liquidity combined)\n", + "# ③ 降低单因子的\"失效期\"风险(某个风格因子可能在某段时间失效)\n", + "# Reduce regime risk (individual factors can fail in certain market regimes)\n", + "#\n", + "# 方法1: 等权合成 (Equal-Weight Composite)\n", + "# composite_EQ = (z_1 + z_2 + … + z_n) / n\n", + "# 优点: 简单、稳健,不依赖历史 IC 估计 缺点: 不区分因子强弱\n", + "# Pro: simple, robust Con: treats all factors equally regardless of efficacy\n", + "#\n", + "# 方法2: IC 加权合成 (IC-Weighted Composite)\n", + "# w_i = max(IC_mean_i, 0) / Σ max(IC_mean_j, 0) (只给正 IC 因子分配权重)\n", + "# composite_ICW = Σ w_i × z_i\n", + "# 优点: 给更强的因子更高权重 缺点: 历史 IC 可能不稳定(过拟合风险)\n", + "# Pro: higher weight to stronger factors Con: historical IC may be unstable (overfitting)\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a132503", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n[§5] 因子合成 / Factor Combination...\")\n", + "\n", + "# 只合成正 IC 的因子(负 IC 因子方向混乱,不宜纳入)\n", + "# Only combine factors with positive IC (negative IC factors are directionally inconsistent)\n", + "positive_ic_factors = {\n", + " name: f for name, f in FACTORS_PROCESSED.items()\n", + " if ic_stats[name][\"IC Mean\"] > 0\n", + "}\n", + "print(f\" IC均值为正的因子: {list(positive_ic_factors.keys())}\")\n", + "\n", + "# ── 等权合成 / Equal-Weight Composite ─────────────────────────────────────────\n", + "composite_eq = sum(\n", + " f.reindex(columns=symbols).fillna(0)\n", + " for f in positive_ic_factors.values()\n", + ") / len(positive_ic_factors)\n", + "\n", + "# ── IC 加权合成 / IC-Weighted Composite ───────────────────────────────────────\n", + "ic_weights_raw = {n: max(ic_stats[n][\"IC Mean\"], 0) for n in positive_ic_factors}\n", + "total_w = sum(ic_weights_raw.values())\n", + "ic_weights = {n: w / total_w for n, w in ic_weights_raw.items()}\n", + "\n", + "composite_icw = sum(\n", + " ic_weights[n] * f.reindex(columns=symbols).fillna(0)\n", + " for n, f in positive_ic_factors.items()\n", + ")\n", + "\n", + "# 计算合成因子的 IC / Evaluate composite factor IC\n", + "ic_eq = compute_ic_series(composite_eq, simple_ret_df, FORWARD_PERIOD)\n", + "ic_icw = compute_ic_series(composite_icw, simple_ret_df, FORWARD_PERIOD)\n", + "\n", + "print(f\"\\n IC 权重分配 / IC Weights:\")\n", + "for name, w in ic_weights.items():\n", + " print(f\" {name}: {w:.3f}\")\n", + "\n", + "print(f\"\\n {'合成方法':22s} {'IC均值':>8s} {'ICIR':>8s}\")\n", + "print(f\" \" + \"─\" * 45)\n", + "print(f\" {'等权 Equal-Weight':22s} {ic_eq.mean():+8.4f} {ic_eq.mean()/ic_eq.std():+8.4f}\")\n", + "print(f\" {'IC加权 IC-Weighted':22s} {ic_icw.mean():+8.4f} {ic_icw.mean()/ic_icw.std():+8.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "eaa4a346", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# §6 多空组合 Long-Short Portfolio\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# 多空组合 (Long-Short Portfolio) 是因子策略的实际收益实现形式。\n", + "# A long-short portfolio is how a factor strategy generates actual P&L.\n", + "#\n", + "# 构建逻辑 / Construction logic:\n", + "# 做多 (Long) = 买入因子分最高的 Top 20% 股票(预期跑赢,做多享受上涨)\n", + "# 做空 (Short) = 卖空因子分最低的 Bottom 20% 股票(预期跑输,做空赚下跌)\n", + "# 多空价差 = 多头组合收益 - 空头组合收益(无论市场涨跌都能赚钱)\n", + "#\n", + "# Long = Buy top 20% stocks by factor score (expected to outperform)\n", + "# Short = Sell short bottom 20% stocks (expected to underperform)\n", + "# L-S = Long return − Short return (market-neutral, profits in any market)\n", + "#\n", + "# 注意 A 股限制 / A-share restriction:\n", + "# A 股融券做空受到严格限制,本演示仅作为量化研究的理论演示。\n", + "# 在港股、美股等市场中,做空非常普遍。\n", + "# Short selling in A-shares is heavily restricted. This demo is theoretical.\n", + "# Short selling is common and practical in HK/US markets.\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48635af1", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n[§6] 多空组合 / Long-Short Portfolio...\")\n", + "\n", + "TOP_PCTILE = 0.20 # 前后 20% / top & bottom 20%\n", + "\n", + "\n", + "def compute_long_short_portfolio(\n", + " factor_df: pd.DataFrame,\n", + " price_df: pd.DataFrame,\n", + " top_pct: float = 0.20,\n", + " rebal_period: int = 21,\n", + ") -> dict:\n", + " \"\"\"\n", + " 构建多空组合,计算每个持仓期的多头、空头、多空价差收益。\n", + " Build a long-short portfolio; compute per-period long, short, L-S returns.\n", + "\n", + " Returns: dict with 'long', 'short', 'long_short' keys, each a pd.Series.\n", + " \"\"\"\n", + " rebal_dates = price_df.index[::rebal_period]\n", + " long_rets, short_rets, ls_rets, ret_dates = [], [], [], []\n", + "\n", + " for i in range(len(rebal_dates) - 1):\n", + " t0, t1 = rebal_dates[i], rebal_dates[i + 1]\n", + " f_today = factor_df.loc[t0].dropna()\n", + " if len(f_today) < 10:\n", + " continue\n", + " n_top = max(1, int(len(f_today) * top_pct))\n", + "\n", + " long_stocks = f_today.nlargest(n_top).index # Top 20% → 买入\n", + " short_stocks = f_today.nsmallest(n_top).index # Bottom 20% → 卖空\n", + "\n", + " p0 = price_df.loc[t0]\n", + " p1 = price_df.loc[t1]\n", + " ret = p1 / p0 - 1.0\n", + "\n", + " lr = ret[long_stocks].mean()\n", + " sr = ret[short_stocks].mean()\n", + " long_rets.append(lr)\n", + " short_rets.append(sr)\n", + " ls_rets.append(lr - sr) # 多空价差 / long-short spread\n", + " ret_dates.append(t0)\n", + "\n", + " return {\n", + " \"long\" : pd.Series(long_rets, index=ret_dates, name=\"Long\"),\n", + " \"short\" : pd.Series(short_rets, index=ret_dates, name=\"Short\"),\n", + " \"long_short\" : pd.Series(ls_rets, index=ret_dates, name=\"L-S\"),\n", + " }\n", + "\n", + "\n", + "def compute_max_drawdown(returns: pd.Series) -> float:\n", + " \"\"\"最大回撤 / Maximum Drawdown\"\"\"\n", + " cum = (1 + returns).cumprod()\n", + " peak = cum.cummax()\n", + " return ((cum - peak) / peak).min()\n", + "\n", + "\n", + "def compute_sharpe(returns: pd.Series, ann_factor: float) -> float:\n", + " \"\"\"夏普比率 / Annualized Sharpe Ratio\"\"\"\n", + " return returns.mean() / returns.std() * np.sqrt(ann_factor) if returns.std() > 0 else 0.0\n", + "\n", + "\n", + "ls_result = compute_long_short_portfolio(composite_icw, prices_df, TOP_PCTILE, REBAL_PERIOD)\n", + "ls_equity = {k: (1 + v).cumprod() for k, v in ls_result.items()}\n", + "\n", + "ann_factor = periods_per_year\n", + "print(f\"\\n {'组合':14s} {'年化收益':>10s} {'夏普比率':>10s} {'最大回撤':>10s}\")\n", + "print(f\" \" + \"─\" * 54)\n", + "for name, rets in ls_result.items():\n", + " ann_ret = (1 + rets.mean()) ** ann_factor - 1\n", + " sharpe = compute_sharpe(rets, ann_factor)\n", + " mdd = compute_max_drawdown(rets)\n", + " print(f\" {name:14s} {ann_ret:+10.2%} {sharpe:+10.3f} {mdd:+10.2%}\")\n", + "print(f\" \" + \"─\" * 54)" + ] + }, + { + "cell_type": "markdown", + "id": "04431d61", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# §7 因子衰减分析 Factor Decay Analysis\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# 因子衰减 (Factor Decay) 描述了因子预测能力随持有期增加而减弱的规律:\n", + "# Factor decay describes how predictive power weakens as holding period grows:\n", + "#\n", + "# 短持有期 H=1D: IC 最高(信号最新鲜,预测力最强)\n", + "# H 增大: IC 逐渐下降(信号被市场消化,噪音积累)\n", + "# H=63D(1季度): IC 接近 0(信号已基本失效)\n", + "#\n", + "# 实践意义 / Practical implications:\n", + "# ┌──────────────────┬──────────────────────────────────────────────┐\n", + "# │ 衰减类型 │ 适合持仓频率 │\n", + "# ├──────────────────┼──────────────────────────────────────────────┤\n", + "# │ 快速衰减 (Fast) │ 日频或周频换仓(换手率高,成本高!) │\n", + "# │ 慢速衰减 (Slow) │ 月频或季频换仓(成本效率更高,适合实盘) │\n", + "# └──────────────────┴──────────────────────────────────────────────┘\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a2292608", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n[§7] 因子衰减分析 / Factor Decay Analysis...\")\n", + "\n", + "DECAY_HORIZONS = [1, 5, 10, 21, 42, 63] # 持有期(交易日)/ holding periods (days)\n", + "\n", + "decay_results = {}\n", + "for fname in [best_factor_name, \"REV\"]: # 对比 \"慢衰减\" vs \"快衰减\" 因子\n", + " horizon_ics = []\n", + " for h in DECAY_HORIZONS:\n", + " ic_h = compute_ic_series(FACTORS_PROCESSED[fname], simple_ret_df, h)\n", + " horizon_ics.append(ic_h.mean())\n", + " decay_results[fname] = horizon_ics\n", + " ic_str = \" \".join([f\"H={h}D: {v:+.4f}\" for h, v in zip(DECAY_HORIZONS, horizon_ics)])\n", + " print(f\" {fname}: {ic_str}\")" + ] + }, + { + "cell_type": "markdown", + "id": "9ecc538c", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# §8 可视化 Visualization\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34a1f89e", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\\n[§8] 生成可视化图表 / Generating visualization...\")\n", + "\n", + "pct_fmt = FuncFormatter(lambda x, _: f\"{x:.0%}\")\n", + "\n", + "fig = plt.figure(figsize=(20, 24))\n", + "fig.suptitle(\n", + " \"Alpha 因子研究演示 | Alpha Factor Research Demo\\n\"\n", + " \"50只合成股票 × 3年日线数据 | 50 Synthetic Stocks × 3-Year Daily Data\",\n", + " fontsize=14, fontweight='bold', y=0.99\n", + ")\n", + "gs = gridspec.GridSpec(4, 3, figure=fig, hspace=0.50, wspace=0.35)\n", + "\n", + "# ── Panel 1: 股票池价格(归一化)/ Universe Prices (Normalized) ──────────────\n", + "ax1 = fig.add_subplot(gs[0, 0])\n", + "norm_prices = prices_df / prices_df.iloc[0] # 归一化 / normalize to 1\n", + "for sym in symbols[:12]:\n", + " ax1.plot(dates, norm_prices[sym], alpha=0.35, linewidth=0.7, color='#1f77b4')\n", + "eq_index = norm_prices.mean(axis=1) # 等权指数 / equal-weight index\n", + "ax1.plot(dates, eq_index, color='black', linewidth=2, label='等权指数 EW Index', zorder=5)\n", + "ax1.axhline(1, color='gray', linewidth=0.6, linestyle='--')\n", + "ax1.set_title(\"股票池价格(归一化)\\nUniverse Prices (Normalized)\", fontsize=10)\n", + "ax1.set_ylabel(\"归一化价格\")\n", + "ax1.legend(fontsize=8)\n", + "ax1.tick_params(axis='x', rotation=25, labelsize=8)\n", + "ax1.xaxis.set_major_locator(plt.MaxNLocator(4))\n", + "\n", + "# ── Panel 2: IC 时间序列(5因子)/ IC Time Series ─────────────────────────────\n", + "ax2 = fig.add_subplot(gs[0, 1:])\n", + "ic_colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']\n", + "for (name, ic_s), color in zip(ic_series_dict.items(), ic_colors):\n", + " rolling_ic = ic_s.rolling(20, min_periods=5).mean() # 20日滚动均值平滑\n", + " ax2.plot(ic_s.index, rolling_ic, label=name, color=color, linewidth=1.4)\n", + "ax2.axhline(0, color='black', linewidth=0.8, linestyle='--')\n", + "ax2.axhline(0.05, color='green', linewidth=0.8, linestyle=':', alpha=0.7)\n", + "ax2.axhline(-0.05, color='red', linewidth=0.8, linestyle=':', alpha=0.7)\n", + "ax2.set_title(\"因子 IC 时间序列(20日滚动均值)\\nFactor IC Time Series (20D Rolling Mean)\", fontsize=10)\n", + "ax2.set_ylabel(\"IC\")\n", + "ax2.legend(loc='upper right', ncol=5, fontsize=8)\n", + "ax2.tick_params(axis='x', rotation=25, labelsize=8)\n", + "ax2.xaxis.set_major_locator(plt.MaxNLocator(5))\n", + "\n", + "# ── Panel 3: IC 均值 & 误差棒 / IC Mean Bar Chart ─────────────────────────────\n", + "ax3 = fig.add_subplot(gs[1, 0])\n", + "names_list = list(ic_stats.keys())\n", + "ic_means = [ic_stats[n][\"IC Mean\"] for n in names_list]\n", + "ic_stds = [ic_stats[n][\"IC Std\"] for n in names_list]\n", + "bar_colors3 = ['#2ca02c' if v > 0 else '#d62728' for v in ic_means]\n", + "ax3.bar(names_list, ic_means, yerr=ic_stds, capsize=5, color=bar_colors3,\n", + " alpha=0.8, error_kw={'linewidth': 1.5, 'ecolor': 'black'})\n", + "ax3.axhline(0, color='black', linewidth=0.8)\n", + "ax3.axhline(0.05, color='green', linewidth=1.2, linestyle='--', alpha=0.7)\n", + "ax3.axhline(-0.05, color='red', linewidth=1.2, linestyle='--', alpha=0.7)\n", + "ax3.set_title(\"因子 IC 均值(误差棒=±1σ)\\nIC Mean ± 1σ\", fontsize=10)\n", + "ax3.set_ylabel(\"IC 均值\")\n", + "ax3.tick_params(axis='x', labelsize=9)\n", + "\n", + "# ── Panel 4: ICIR 柱状图 / ICIR Bar Chart ──────────────────────────────────────\n", + "ax4 = fig.add_subplot(gs[1, 1])\n", + "icirs = [ic_stats[n][\"ICIR\"] for n in names_list]\n", + "bar_colors4 = ['#2ca02c' if v > 0 else '#d62728' for v in icirs]\n", + "ax4.bar(names_list, icirs, color=bar_colors4, alpha=0.8)\n", + "ax4.axhline(0, color='black', linewidth=0.8)\n", + "ax4.axhline(0.5, color='green', linewidth=1.2, linestyle='--', alpha=0.7, label='ICIR=0.5')\n", + "ax4.axhline(-0.5, color='red', linewidth=1.2, linestyle='--', alpha=0.7)\n", + "ax4.set_title(\"因子 ICIR\\nFactor ICIR (Mean IC / Std IC)\", fontsize=10)\n", + "ax4.set_ylabel(\"ICIR\")\n", + "ax4.legend(fontsize=8)\n", + "ax4.tick_params(axis='x', labelsize=9)\n", + "\n", + "# ── Panel 5: 因子相关矩阵 / Factor Correlation Matrix ─────────────────────────\n", + "ax5 = fig.add_subplot(gs[1, 2])\n", + "# 用各因子的截面均值时序来衡量因子间相关性 / Use cross-sectional mean timeseries\n", + "factor_ts = pd.DataFrame({n: f.mean(axis=1) for n, f in FACTORS_PROCESSED.items()})\n", + "corr_matrix = factor_ts.corr()\n", + "im5 = ax5.imshow(corr_matrix.values, cmap='RdYlGn', vmin=-1, vmax=1, aspect='auto')\n", + "ax5.set_xticks(range(len(names_list)))\n", + "ax5.set_yticks(range(len(names_list)))\n", + "ax5.set_xticklabels(names_list, fontsize=9)\n", + "ax5.set_yticklabels(names_list, fontsize=9)\n", + "for i in range(len(names_list)):\n", + " for j in range(len(names_list)):\n", + " ax5.text(j, i, f\"{corr_matrix.values[i, j]:.2f}\",\n", + " ha='center', va='center', fontsize=8,\n", + " color='black' if abs(corr_matrix.values[i, j]) < 0.6 else 'white')\n", + "plt.colorbar(im5, ax=ax5)\n", + "ax5.set_title(\"因子相关矩阵\\nFactor Correlation Matrix\", fontsize=10)\n", + "\n", + "# ── Panel 6: 分层回测累积净值 / Quantile Cumulative Returns ───────────────────\n", + "ax6 = fig.add_subplot(gs[2, 0])\n", + "q_colors = plt.cm.RdYlGn(np.linspace(0.05, 0.95, N_QUANTILES))\n", + "for q in range(1, N_QUANTILES + 1):\n", + " ax6.plot(quantile_cumret.index, quantile_cumret[f\"Q{q}\"],\n", + " label=f\"Q{q}\", color=q_colors[q - 1], linewidth=1.5)\n", + "ax6.plot(ls_cumret.index, ls_cumret, label='L-S', color='black', linewidth=2, linestyle='--')\n", + "ax6.axhline(1, color='gray', linewidth=0.6, linestyle=':')\n", + "ax6.set_title(f\"分层回测({best_factor_name})\\nQuantile Returns ({best_factor_name})\", fontsize=10)\n", + "ax6.set_ylabel(\"累积净值\")\n", + "ax6.legend(ncol=3, fontsize=8)\n", + "ax6.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f\"{x:.1f}x\"))\n", + "ax6.tick_params(axis='x', rotation=25, labelsize=8)\n", + "ax6.xaxis.set_major_locator(plt.MaxNLocator(4))\n", + "\n", + "# ── Panel 7: 分位年化收益柱状图 / Quintile Annualized Return Bar ───────────────\n", + "ax7 = fig.add_subplot(gs[2, 1])\n", + "ann_q_rets = [(1 + quantile_rets[f\"Q{q}\"].mean()) ** periods_per_year - 1\n", + " for q in range(1, N_QUANTILES + 1)]\n", + "bar_colors7 = plt.cm.RdYlGn(np.linspace(0.05, 0.95, N_QUANTILES))\n", + "bars7 = ax7.bar([f\"Q{q}\" for q in range(1, N_QUANTILES + 1)], ann_q_rets, color=bar_colors7)\n", + "ax7.axhline(0, color='black', linewidth=0.8)\n", + "ax7.set_title(\"各分位组年化收益\\nAnnualized Return per Quintile\", fontsize=10)\n", + "ax7.set_ylabel(\"年化收益 / Ann. Return\")\n", + "ax7.yaxis.set_major_formatter(pct_fmt)\n", + "ax7.tick_params(axis='x', labelsize=9)\n", + "for bar, val in zip(bars7, ann_q_rets):\n", + " ax7.text(bar.get_x() + bar.get_width() / 2, val,\n", + " f\"{val:.1%}\", ha='center',\n", + " va='bottom' if val >= 0 else 'top', fontsize=8)\n", + "\n", + "# ── Panel 8: 多空组合净值曲线 / Long-Short Equity Curve ───────────────────────\n", + "ax8 = fig.add_subplot(gs[2, 2])\n", + "ax8.plot(ls_equity['long_short'].index, ls_equity['long_short'].values,\n", + " color='purple', linewidth=2, label='多空 L-S')\n", + "ax8.plot(ls_equity['long'].index, ls_equity['long'].values,\n", + " color='#2ca02c', linewidth=1.5, linestyle='--', label='多头 Long', alpha=0.8)\n", + "ax8.plot(ls_equity['short'].index, ls_equity['short'].values,\n", + " color='#d62728', linewidth=1.5, linestyle='--', label='空头 Short', alpha=0.8)\n", + "ax8.axhline(1, color='gray', linewidth=0.6, linestyle=':')\n", + "ax8.set_title(\"多空组合净值曲线\\nLong-Short Portfolio NAV\", fontsize=10)\n", + "ax8.set_ylabel(\"净值 / NAV\")\n", + "ax8.legend(fontsize=8)\n", + "ax8.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f\"{x:.1f}x\"))\n", + "ax8.tick_params(axis='x', rotation=25, labelsize=8)\n", + "ax8.xaxis.set_major_locator(plt.MaxNLocator(4))\n", + "\n", + "# ── Panel 9: 因子衰减曲线 / Factor Decay Curve ────────────────────────────────\n", + "ax9 = fig.add_subplot(gs[3, :])\n", + "decay_colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728']\n", + "horizon_labels = [f\"{h}D\" for h in DECAY_HORIZONS]\n", + "for (fname, decay_ics), color in zip(decay_results.items(), decay_colors):\n", + " ax9.plot(range(len(DECAY_HORIZONS)), decay_ics,\n", + " 'o-', label=fname, color=color, linewidth=2, markersize=7)\n", + " for i, (h, v) in enumerate(zip(DECAY_HORIZONS, decay_ics)):\n", + " ax9.annotate(f\"{v:+.3f}\", (i, v), textcoords=\"offset points\",\n", + " xytext=(0, 8), ha='center', fontsize=8, color=color)\n", + "ax9.axhline(0, color='black', linewidth=0.8, linestyle='--')\n", + "ax9.axhline(0.05, color='green', linewidth=0.8, linestyle=':', alpha=0.7, label='IC=0.05 参考线')\n", + "ax9.set_title(\n", + " \"因子衰减曲线 Factor Decay Curve\\n\"\n", + " \"IC 均值随持有期延长的衰减 | How Mean IC Decays with Longer Holding Period\",\n", + " fontsize=10\n", + ")\n", + "ax9.set_xlabel(\"持有期 / Holding Period\")\n", + "ax9.set_ylabel(\"IC 均值 / Mean IC\")\n", + "ax9.set_xticks(range(len(DECAY_HORIZONS)))\n", + "ax9.set_xticklabels(horizon_labels)\n", + "ax9.legend(fontsize=9, ncol=4)\n", + "ax9.grid(True, alpha=0.3)\n", + "ax9.set_xlim(-0.3, len(DECAY_HORIZONS) - 0.7)\n", + "\n", + "plt.savefig(\"alpha_factor_demo.png\", dpi=150, bbox_inches='tight')\n", + "print(f\" 图表已保存: alpha_factor_demo.png / Chart saved: alpha_factor_demo.png\")\n", + "\n", + "print(\"\\n\" + \"=\" * 70)\n", + "print(\" 演示完成! Demo Complete!\")\n", + "print(\" 输出: alpha_factor_demo.png\")\n", + "print(\"=\" * 70)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (trading)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/quant_data_pipeline_demo.ipynb b/quant_data_pipeline_demo.ipynb new file mode 100644 index 0000000..2d01f3e --- /dev/null +++ b/quant_data_pipeline_demo.ipynb @@ -0,0 +1,1138 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "37aee204", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# Quantitative Trading — Data Pipeline Demo\n", + "\n", + "# =============================================================================\n", + "# Topics covered:\n", + "# 1. Price Adjustment for Corporate Actions (splits & dividends)\n", + "# 2. Simple Returns vs Log Returns\n", + "# 3. Multi-stock Panel & Missing Value Handling\n", + "# 4. Outlier Detection & Treatment (Z-score, MAD, Winsorize)\n", + "# 4b. Circuit Breakers & Price Limit Flags (涨跌停) [A-shares]\n", + "# 5. Trading Calendar & Cross-market Alignment\n", + "# 6. End-to-End DataPipeline Class\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "32497e8a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "环境准备完成!\n" + ] + } + ], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "from scipy import stats\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "# 使用我们在 Docker 中配置的字体\n", + "plt.rcParams['font.sans-serif'] = ['WenQuanYi Zen Hei', 'Arial Unicode MS', 'SimHei', 'DejaVu Sans']\n", + "plt.rcParams['axes.unicode_minus'] = False\n", + "plt.rcParams['figure.figsize'] = (12, 6)\n", + "plt.rcParams['figure.dpi'] = 100\n", + "\n", + "np.random.seed(42)\n", + "print(\"环境准备完成!\")" + ] + }, + { + "cell_type": "markdown", + "id": "998e880d", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# TOPIC 1: Price Adjustment for Corporate Actions (价格复权)\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# Raw stock prices have artificial jumps caused by stock splits and cash\n", + "# dividends. These must be adjusted before computing returns or signals,\n", + "# otherwise your algorithm will see fake crashes/spikes.\n", + "#\n", + "# Key concepts:\n", + "# - Unadjusted price (未复权): raw market price, has visible jumps\n", + "# - Forward-adjusted (前复权): history scaled to today's price level\n", + "# - Backward-adjusted (后复权): history kept real, recent prices scaled up\n", + "# - Adjustment factor (复权因子): multiplier that accounts for all events\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "ed3f078e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-------adj factors------\n", + "2020-01-02 1.0\n", + "2020-01-03 1.0\n", + "2020-01-06 1.0\n", + "2020-01-07 1.0\n", + "2020-01-08 1.0\n", + " ... \n", + "2023-10-26 1.0\n", + "2023-10-27 1.0\n", + "2023-10-30 1.0\n", + "2023-10-31 1.0\n", + "2023-11-01 1.0\n", + "Freq: B, Length: 1000, dtype: float64\n", + "-------end of adj factors------\n", + "公司行为事件:\n", + " date type ratio amount\n", + "2021-02-25 split 2.0 NaN\n", + "2020-03-26 dividend NaN 0.5\n", + "2020-09-17 dividend NaN 0.5\n", + "2021-03-11 dividend NaN 0.5\n", + "2021-09-02 dividend NaN 0.5\n", + "2022-02-24 dividend NaN 0.5\n", + "2022-08-18 dividend NaN 0.5\n", + "2023-02-09 dividend NaN 0.5\n", + "2023-08-03 dividend NaN 0.5\n", + "\n", + "数据形状: (1000, 7)\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABW0AAAQBCAYAAABR1qgmAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3QV8W+X6wPEndZ116wTmTIExgwkyBgx3h6EXl11chl6ci3Ph4vZH70UHDLcN3e5gDAZj7tpOKuvqzf/zvGcnTdKkTdOksd93nyxpkp6cPnmTnPPkOc/rcDqdTgEAAAAAAAAARIWkSK8AAAAAAAAAAKAeSVsAAAAAAAAAiCIkbQEAAAAAAAAgipC0BQAAAAAAAIAoQtIWAAAAAAAAAKIISVsAAAAAAAAAiCIkbQEAABATysrKIr0KAAAAQKsgaQsAAJBAXnvtNdm8eXOLlzN58mS56KKLJNTuvPNOueSSS6Surs7j+i1btkjv3r3l6aefbtbyzjnnHLn66qsbXD937lw58sgjZc6cORJOZ555pvzrX/9q9D76fBQXF/u8bdu2bTJ//nwpKioK0xoCAAAgGpG0BQAASBALFiyQ0047TW666aYWL+unn36Sb775RkLt9ddfl2+//VaSkjw3Ux9++GEpLCyUzz77rFnL++qrr+T7779vcL0u58MPP/SbLA2V999/X/73v/81ep/BgwfLUUcd5fM2/d1BgwbJf/7znzCtIQAAAKJRSqRXAAAAAK3jiSeeMOea/Lz++usD+p0xY8b4TSjaNCn65ZdfNrmsPn36yBlnnOH39hUrVshff/1lqnjdrV+/3lSrZmZmynvvvSdTpkyRo48+Wlpi6tSpruTzr7/+6nHbQQcdZBKltvLyclmzZo3fZaWkpEivXr1atD4AAACAO5K2AAAACUCToU8++aS5/Pbbbwf8e5MmTQooaXvbbbc1uaz999+/0aTt//3f/5nziRMnuq5zOp3md6qrq02CVZO12pZhjz32kG7dukkwFi1aJNOnTzeXvRPEavHixR4///DDDzJhwgS/y+vbt2+D3wEAAABagqQtAABAnNOE5/nnny/Jycmml+uAAQNCunztQattFzS5unr1avn66689bq+pqTGVqz169PC7jKqqKpNUHjFihOy8886u6//5z3/KF198IQ888IDstttu8sorr8i+++4rhx12mGmjkJub2+z1ffTRR13JVl2uHaNzzz1XhgwZYq53t+uuu8obb7zhc1n33HOPpKWlyTPPPCMLFy5scHtFRYXMnj3b9NXdfffd5aSTTmr2+gIAACDxkLQFAACIY1qpevbZZ7v6ug4cODCg37v11lvlH//4h8d1L730klmWO4fDYc6/++47GTt2rEliaiK1bdu2Hn1ZNXk5fvz4RidI0zYIt9xyi+u6Bx980FTCHnfccXLllVea6/baay/TKkETxZq41XYJeXl5HsvSpK5dSWu3XdD13GGHHUyPW53MTH9esmSJdOzY0Szz7rvvlpKSErnxxhsbrFvnzp3l5JNP9hun/v37y5tvvmmW7a/KWU86KVmokrY6OZkuSx//2GOPDckyAQAAED1I2gIAAMQpTUKec845ph2CJgy1MlWTuHqdP0VFRSZZ6ssuu+wiV111lbn81FNPSXp6uiuJu+OOO8o+++wjd911l5mgzL3nrF156y9pq5OB3XDDDebysGHDTA9ZnSztoYceMj11X375ZVdyWF188cWydu1a81jaJuGdd96RoUOHum4/9dRTZfTo0VJbW2sqabWNwumnn2564p511llSV1dnYqLLueCCC0wSWBPUJ5xwghx88ME+11GXpYnd448/3tXvVqtzly5dapKnt99+u8/fa9eunRx++OHy6quvSmM0gazVuO50fbWlhDeNh7at0OpkPQEAACD+kLQFAACIU/fff79JTmpiUich04rQbdu2mRYG/pSWlvq9beTIkeakiUpN7Pbu3VvuuOMOk1xUXbp0kTZt2si7777rkbTVicP09zSx68vNN99sqmyVVquecsopsnz5cjniiCNMMlaTpXfeeafr/poA1b9Bk7qa6NRln3feeWadsrKyTCsI9dtvv5mkrSZpR40aZdoY6HK0mlerU7VdhK7ngQceKP369TMtDvzRicieffZZeeSRR+STTz4xyWLtdautH3TZLaV/j3eyXKuV3ZO2WrGsyW2N0U477SSvv/66abkAAACA+EPSFgAAIE5pQnXcuHFywAEHuK5bt26daQ/QEm+99ZYrkdm9e3dTqarVrRkZGaaVgVa+bt26VXJycmTZsmUm2ahtE3zRBO/jjz9uKmm1ClhbEWj1qCZHtZp0v/32k40bN3okbWfMmGEmLdP7a2WuVsxqYlYTtu60ZYPatGmTSdJqglfbFGjCUytn//jjD5O41cvab7esrMxUxvqit2uLCZ2QTOOpidMPP/xQUlJSTIWxVihfc801pkK4Z8+ezY6pPk/Tpk1rcL19nS5X/w6tbtZqZ63s9f57AQAAED+SIr0CAAAACB/3hK3ac889ZcuWLX5PWp3aGK0s1apd1b59e9O+QCcg++WXX8x1F110kWnLoIfwK61O1cToxIkTGyxrzpw55nc1+XjZZZe5WjBoD1r9OSmp6U1V7V+r66yTmHnTHrxq+PDhJsmpVbc//vijfP755ybZq4lQTeZqElvXf/DgwfLYY4+Z/rv+Erfa+kGT0VoFrFXMmrDVHr6VlZUmiautEBqrVg6WxkKTtTrZmf4dJGwBAADiG0lbAACABKKH9Guy1d9pt912a/T3tcJVD+XXBGZqaqq88sor5vf+/ve/m8pXPVxfE5n33XefFBYWmoToMccc47M1gvaa1fYEOomX++Nq9Wpz6Hp4J3h/+uknVyJZ6URmWiWrFbwHHXSQST5PnTrVPLa2U9AK3EMPPdQki7t27SoXXnih/Prrrz7XWSc/08ralStXmgpjpRXCGos///zTtGoINTvprHEHAABA/KM9AgAAQII499xzTauB999/37RJ0EnE9HB7X8aOHdvgOq3EnTx5sqkm1cnDtA+tthPQ1gd5eXmuycJ0Ui9ta6BVsPo71113nc/HyM/Pd03QZVfF+hJIxa07TR5ffvnlpiK2Q4cO5jptgaCtGrT/7pFHHikDBgww7RPsFgpK2xocddRRssMOO5gErN5PK3K9aQ9bTfx+8MEHJmFs06SwTvL23HPPmSSx9r0FAAAAgkHSFgAAIEFcf/31pi+rtgDQ5GV2dnbAv6s9Y08//XSThNXJsNwTsZoMdjd+/Hg55JBDzIRdWok6YsSIoNdZH7djx47N+h2tptU+ujoR23/+8x9znU6QptdpVfBZZ51lbmss6auTgvlLaC9YsEA++ugj08NXk9jaYkETxErj8uKLL5rrte8tAAAAEAyStgAAAAlCq0e156xO9KWnf/7zn37vqxWkBx54oOtnnQRryZIlJpmplaaN0Spc7Uurtm3b1qJ1Xrp0qanaDZROUnbttdeaKldtdWAnbZUmbG2asNbJ0txpSwR7kjZ/CVulE5pp0lvbK+i66SRp9957r7lNJznTx/VVqRxq+nxoBXH//v3D/lgAAABoXfS0BQAAiHPz5883LQ100i/tvaptDHRCMk2salWpnjSJqxNz6W06IdfFF1/ssYxOnTqZHrHau7YxZWVlpoetVqNqBeqUKVNcCc3m+vjjj2XNmjWmcjdQQ4cOlUsvvVTeeustj9YFofLUU0+ZZO0VV1wh48aNk+OOO04ef/xx0+PWpoltvT7cvvjiCxk0aJDpUwwAAID4QtIWAAAgzmm16x9//GGSiZpMfffdd83EW7vssos5jP+1114zl7Vi9Pjjj5cff/zRTMblLSsrq9HH0R63Wnn6zTffmBYMb7/9tpncSx/jiSeeaNY66yRmmnxt27atnHTSSQH/nrYsePjhh8MyYdenn35qeuUOGTJEbrzxRnOdVi5rovr5558PyWNoIl3jpq0sNPndGJ30TNtHdO/ePSSPDQAAgOhBewQAAIA4N3z4cNNmwJ7QS/vNPvPMM6a3q3sV7IknniiPPvqoSZQ2l1bUnnfeebJ582Z58sknTasBpRWvBx98sFxyySUmIXn33XdLcnJyo8vSZehEXzpxmCZDm0oWB0MrizUx6m7mzJl+76+Tt5188smmN+6bb77pap+gk63pxGWaBG8unQzu119/lfLycpk9e7apZtaJ4myaOLdjpdXSWhFtT/amFcgac51QLRwJagAAAEQWSVsAAIA4pclTbWlQXV0tJSUlJhm6cuVKc9IKzZSUFDnggANMclAP+ddkpJ46dOggvXr1MsnIvLw82W233UyFqS+aPDz77LPNofp6X12OJoVtmnDVClWdxOy+++6T6dOnm6pbTST7osvRtgqa0Lztttvkb3/7W1hiozFprKevu5tvvtlUIeuEaLp+mii1aRL1999/NzFzb+ug1crankGrnLX/rbfly5eb5WjyWOlz0bdvX/O377777qYn784772z6A2sSXZPpevL26quvBhkBAAAARDOStgAAAHFK+7tqhatWaObm5pqKTD20/9RTTzWTie21116uZKNOaDVnzhz56aefZO7cuaayU8/Xrl0rRx55pN/H0B65mpzUFgYPPfSQdOvWrcF9NHGrh/zr7bfeeqssXrzYb9LWbsvwwgsvmGSwL7q8YKqB3fmaiOzKK680j+tNq2m//fZbM5Gbr6pW94St0iTtXXfdZS5r8ttXwluv1+S1JtM1Qavx8JXc1WVrBfB7773nsb5aNa2TnWkVMwAAAOKPw6lb8QAAAIhLWl2rh/RrJWcw3A/J90erdu3WC03RJKWuT2MqKytd7QcAAACARETSFgAAAAAAAACiSGAlEQAAAAAAAACAVkHSFgAAAAAAAACiCElbAAAAAAAAAIgiJG0BAAAAAAAAIIqQtAUAAAAAAACAKELSFgAAAAAAAACiCElbAAAAAAAAAIgiJG0BAAAAAAAAIIqQtAUAAAAAAACAKELSFgAAAAAAAACiCElbAAAAAAAAAIgiJG0BAAAAAAAAIIqQtAUAAAAAAACAKELSFgAAAAAAAACiCElbAAAAAAAAAIgiJG0BAAAAAAAAIIqQtAUAAAAAAACAKELSFgAAAAAAAACiCElbAAAAAAAAAIgiJG0BAAAAAAAAIIqQtAUAAAAAAACAKELSFgAAAAAAAACiCElbAAAAAAAAAIgiJG0BAAAAAAAAIIqQtAUAAAAAAACAKELSFgAAAAAAAACiCElbAAAAAAAAAIgiJG0BAAAAAAAAIIqQtAUAAAAAAACAKELSFgAAAAAAAACiCElbAAAAAAAAAIgiJG0BAAAAAAAAIIqQtAUAAAAAAACAKELSFgAAAAAAAACiCElbAAAAAAAAAIgiJG0BAAAAAAAAIIqQtAUAAAAAAACAKELSFgAAAAAAAACiCElbAAAAAAAAAIgiJG0BAAAAAAAAIIqQtAUAAAAAAACAKELSFgAAAAAAAACiCElbAAAAAAAAAIgiJG0BAAAAAAAAIIqQtAUAAAAAAACAKELSFgAAAAAAAACiCElbAAAAAAAAAIgiJG0BAAAAAAAAIIqQtAUAAAAAAACAKELSFgAAAAAAAACiCElbIAbMnz9ffvrpp4g9fk1NTciWtXHjxgbXFRYWyqJFiwL6fafTKVu3bpXa2lqP6/W6UK4nEoOOmaKiIqmrq5OysjLZtm1bpFcJAADECLaRG9Jt8jlz5khVVZVESiLsb1RWVkZ6FQC0ApK2QAx45JFH5Oyzzw7qdy+66CJ54403gn7s77//XlJTU2Xx4sXSUgsWLJCuXbvKL7/84nH9XXfdJUOGDAlo42PFihWSm5srH374ocf1//rXv6R3794SzTQxWFFR0eRJ79eYv/76S44++mgpKChocoPz008/leOOO05WrVrlcVtxcbHZoHU/2Y+7evXqBqdIbng/9thjsnDhwrAsW8d3+/btZeXKlXLUUUfJxRdf7PN++vfrlyfep/Ly8gb31Q36F198UUaOHGliBwAAWk63ax544AGZOHGiz+0STRQecMABsmbNmoCWxzZyy+kX3t5+/vlnGTZsmKxdu9bjev2SvDWe22jf39D9uttvv71Fy3jqqadkwIABDf5GAPEnJdIrACB8li9fLs8884xJSunGU1P0w9/hcPi9Xb+h1o2YxvTq1Us6duzo87aPP/5Y0tLSZPDgwa7rdIPu6aefNsnK559/3m/SrCmanNQNtD/++MPj+u7du0vbtm0DWoYm7jROurGpG2TTpk2T8ePHB7U+utH56quvelz37bffBrS8b775Rvbdd1+/t2/atEnef/99s9GndON26dKlJlmoiVxNJurG7WeffWZ+PvTQQ6W0tNRjGZqgnD59usd1y5YtM8+fxszbd999J3vttZfP9dExdu+995p18LZlyxa588475ZZbbnE9D/PmzTPP8+OPPy677LJLo7HQLwsuu+wyefnll6V///6yefPmJpPV/fr1k+TkZNfPAwcONBvwtuzsbBOr5tC/bdCgQQ2uf+WVV1xVurquv/32m4mVftFxzjnnmPFuW7Jkiey+++7y+++/y4477tisxwcAINHpNmq3bt3k+uuvl8zMTHnuuec8br/11lvNl7x5eXlNLott5JZtI9vbNaNGjTIJyKa23/VIpr333ttsB2viM5zPbTTsb+g+wOmnn97osvRv8kUTxYcffnijv3vhhReaquHTTjvNbFfqdqc/bH8CsY2kLRBl9Btk7wSbfjNtV/q50w/ovn37+l3WddddZxJK99xzjzk1RTeodEPJn3feecdUJTTm2WeflXPPPdfnbW+99ZapELUfQ79V1/v26dNHxo4da9b3oIMOavRv8kU3WrRiUpe36667etymMQt0g3TSpEly6qmnujZGNdmnFZOB0G/tdQPq/PPPlzFjxjT6N2hSz1fSbv369eZ3m0uToHZSXmO7ww47yG677WbiqVW2PXv29Pl7eptWNcyYMUNOOeUUj9uefPJJsyGoG+k777xzo4//xRdfmOoHXx5++GGz4X3zzTe7rtPEsCY3P/rooyaTtrph36NHDzn55JNdCeLJkyc3+jvr1q2TLl26eFynz40mf1977TWzTvrlQ2PjQjeA9fl3T7qqL7/8Ujp37mw2/nWstmnTxmyUa8Wt3v/PP/804+juu++WrKwsj9/VMXHsscea2997771G/wYAAFD/ua5HCKnhw4fLmWeeaRJvuu0xdOhQV6Lrgw8+MF8Ia0LWpp/1mmTzxjZyy7aR7e0avf6SSy4xicEHH3zQ7+/rdph+Aa77AuF+bqNhf+PII480R8b5ctVVV5mihhdeeMHn7e7FE2+//baccMIJja6X97aqTY+01O1ntj+B2EbSFogyuuGjVZS+eFf6aXLO3+HX+iH/5ptvmmpM3TAJREZGhuuyfci8Ju2UbmhplaBusIwbN85sVLl/S61VuLqxpQk2X2bNmmX68rp/q6wJw88//9xsAGmS8YcffjBVoT/++GODb9I3bNhgNnDsw6L0XDeQdENEk3C6AXXSSSfJWWedZW7XxJ7+PVo9HAh9bE0i6kanTRN/9vKaopWbukGqVQSa7GyMJmw1cakblVdccYXrOUxJafwt2Y6B/ZzoumrFgN2HdebMmbLHHntIoHJycsx6uG8A27Q6W2/3Tjx60y8FtDL4iSeeaHDbr7/+Kvfdd5/ccMMN0q5dO9f1ukyNlT5v5513nnTo0MHnsrWqVpPmmgBNSkoyf69WYOjJpsvVDXEdS43R8aRJVU246jrrONtpp51M/L3pWNLn8LDDDmvwZYeOJ33+NDZK113HolbvapWIfpGih975i5uuu1YC6xjXHQcAANA43ab7v//7vwbXe3/hrC699FKPn3V79aWXXvK4jm3klm8j23S7XreLdFssPT3dbLO5020u3V/QI5M0SanbYuF8bqNlf0O/1NeT0n0n7964vug2ufu+mDstdtDkrG736z6ZexWxNy300b/THdufQAxzAoh6F1xwgXPAgAEB3//LL790pqenO9PS0pz6Mm/q9M033zRYxq233urzvrNmzXIee+yxzgMOOMDj/m+99ZYzNTXVuXXrVp/rdPzxxzu7d+/urK2tNT8/9NBDTofD4Xzsscdc91m4cKGzY8eOzmHDhjlXr17dIAa+1uf99993JicnO3v37u1s3769c/369c53333X3Pbpp58GHLMjjjjCefjhh/u9/fXXX3e+8sorDU6//PKLub20tNQ8pl7nj8ZZ77Ns2TLz84svvmh+tq1atcrv89FYDB5++GFz/uuvvwb8944bN848j4sWLXK++uqrrvVas2aNxzrodfrzd9995/zss8+ckydP9ljOzz//bJ7HjRs3elyvf0ufPn2co0ePdlZXVzd4/LKyMnO7rkdxcbHPdbz++uvNmKmoqDDx1+d58eLFHvdp27at86qrrmr0b9XXzo033mgu63jLzs52zps3z5mSkuK8+eabXX/7/vvv7zzzzDOdTz/9tDMzM9PEwvbXX3+Z++lzoI933nnnueLiTq/7v//7v0bXZ+zYsc4TTzyx0fsAAIDg6Of3e++9Z7YfvLGNHPptZHXDDTeY5X744Yce27sPPvigufzAAw84w/3cRuv+hm6rBrI/9tNPPzX43c8//9y58847OysrK83P//rXv5wZGRmu/Y3777/fudtuu3n8jt5Xf+eTTz7xuJ7tTyA2kbQF4ixpqxtFmow67rjjnFdffbWzc+fOZuPJ10mTS40lCe2NAb3P0qVLXdfpho8mhTXxZjvrrLOc++67r89lfPHFF2YZmqDbtGmTK+F1yy23NLjv//73P2deXp6za9euzo8++qjB7VOmTDG/+9prr5lkX//+/Z2DBg0yy+3Vq5dzzJgxztzcXOfZZ5/tDNTatWvNBt1LL73k9z769/rawLIThq2RtLVpotB9OZqs1Z91I1aTi75Oy5cv91iGPhfef4su74orrnDm5+c7y8vLPdbpueeec1588cVmA9fdPffc0+C6BQsWmI3agQMHOtetW+f375g9e7bZ8NWNzblz53rcVlBQYJKr+ribN282y9OdBm+BJm3d/05drtpvv/2ce+yxh0fS9rDDDjPL9E686k7CSSedZE5ZWVlmY1gvz58/33Wfqqoq19hsjL5+9DVaWFjY6P0AAED9Z+zEiRN9njRBp4kq+3NVCwn089h7G4Rt5PBsI6u6ujqTxPTe3q2pqWkyqRmK5zaa9zd0u/Kmm27yu42u2+/+krbu26F33HGHKYTQRLQmkKdNm2au22mnnZyBYPsTiE0kbYEosmTJEp8f5poc0qSVvw973dixff/9987bbrvNbORodWGwlbbu38rqfY455hjX/XRDSTcW7I0z3ZjSDZ/HH3+8we/rhk6/fv3MMrTqUqsrNWn2/PPPOydMmOC88MILXfedPn26SY7qRsiuu+7q85t5+5t8vd8zzzxjNlb+/PNPc9vdd99tbtMNMPeEclNeeOEF83v6zbs/+nhagakbS/apZ8+eLUra2o8bqqRtY6dRo0Z5LEM3aA888ECzgasx1vto1ak+rx988IHrfrqxbY8BTVZqpYE7TXRec801rp/178/JyXGOHDnSVCH7G7P26auvvnLusssuzqSkJOddd93lWo6OYX1MHTt6m1aN6/2DSdrqFw76u926dXNecsklJqmsdMPb/TmxK2197Qjo82vHW593rUTfsmVLQK8vTWy7s5+v//znP42uNwAAsOiXyfb2jBYm6Gn33Xc31+k2mZ3M089rf4k9tpFDu418zjnnBLQd5H7SStxwPLfRvL/Rkkpb248//ujs0KGDOdJR9/suuugi56OPPuq8/fbbnYMHD3YGgu1PIDbR0xaIIgceeKDPflE2X7PXq2XLlpk+SGrPPfc0J/e+rN5N/23aG3XixIl+H2/u3Lmm35PSiZaOOOIIM0GTTkKg/T7vv/9+OeaYY8wEZTp52vHHH+/x+9q/SXtSae8l7WGlva50NlWdHVYnx3r55Zelurrao++VTuilfaNmz54tr7/+umsCKps9gdP+++8vF1xwgUyZMsX0CNXeVXfeeafpq6uTQekEBrp87VvaFO3Jmpub2+R9ta9qU31nm0Njqn1QNW46SZb75A6B0L9T+3TZfav+97//mb7DSv9+7THm3evLnU7eoPez/yadYEIng9Ceyu59lXXc6YRh2rc2OTnZdb321dK+YNdee63rOo2h9qu966675IwzzpD//ve/jf4NOomX9h/T3rE6aYNNx5fO0KuTMWgvMO0Z5t0HLVDTp0+Xvfbay/Sb7dixo/Tv39/Vj1bHm7vFixd7/I3u/e/OPvts08vMpr3K3CeZ+OWXX0wvXJ34Qu+rk3Oo/Px8j2Xp5BXa8/arr74yfdEAAEBgLr/8cte2oW5T6nZEoNhGDu02sm53X3311QHH399+TEuf21jY37jpppv87nNpz1zdn2qMTlSs+2SjR4822932XBJ6WedVCATbn0BsImkLRBFNGLl79tln5fzzz5cRI0aYib50g0MTYrfcckvAy1y/fr2ZHCsYmkjTicU0uavN93Xme02e/ec//zEbafvss49JPOlssSeeeKKZ5Mn7sQsKCsxsrzNmzDATXulGRyA0kahJP3c6sYCdJNMJrHRDTJNpOtGVJrt1I0o3XubNm2eSZzohlG4gXXTRRWbjyp9Vq1aZSd00gdqadHIDTSJq0lsTt4EkbXVDUyf4UocffriMGjVK9ttvP/OzTkimG48aF429Jm0boxM/uE/+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grHpNr9uR2ucdVtU99l0GdoWwbtQRIs0NMmuRwNqIlcLdNwndGP7E4hRTgBR7+abb3YOGDAgoPved999Tn1pp6amOrt37+7cd999nZdcconz5Zdfdq5du9bcZ9u2bc4vvvjCefvttztPOOEE52677ebMy8tzHnzwwQ2W991335nlLViwwHnccceZ+69bt87nY+vy999/f+eYMWOcFRUVHredeeaZzrFjx5rHOPnkk806ePv222+d2dnZzjVr1vj9+7Zs2WLW57333nNdd8455zj33ntvZ3l5uev00ksvOdu2besM1DHHHOPcc889nc21xx57mPXR06BBg5ylpaUet+vfeeKJJzrvvPNO54oVK3wuo7Ky0sT3r7/+cp0WLVrkrK6ubnBfXUd9rv7+9787f/vtN+d+++3n3LBhQ4P76fOgMfdWV1dnnp9bb73V/Hz66aebddflTp482ZmRkWGWXVhY6Pqd2bNnO/faay9zP33+GzN9+nRnjx49nCkpKc5rrrnG/G324x577LHmtrS0NGefPn2c33zzjTMSHn74YXPyZenSpSb+7n+/qqmpaXDfu+++23nYYYc5S0pK/D7WJ5984hw5cqTHa+bQQw814w0AAARGtyf69u3rnDp1qvlsdTgcZpvxxhtvdN3no48+Mtsqui1z2mmnNVgG28ih3Uaura0126qBnvS5+fDDD8Py3Ebb/sYLL7zg7N27t1lGhw4dnFdffbXP7XXb999/b7YP9f433XST6/qCggLn6NGjzd/1/vvv+9w38KbbrGeccYaJpTu2P4HY5ND/Ip04BtA0fakG0l9Je05pFW1jh/aEcx30PvpNsr/2Dlot2ti345Eyffp0M+GVHr7VWK/XeKAVwloNq5N7eVdTa6sBu12AN21RoJXg2p7DH31+dcbdk046yfRBi3dazdFYiwhvWgGsh7Rpr2I9vA0AALTO5zLbyLG9jdycba5I729o6witiNUjzHReEbsdWlO07ZkewaVHjYUS259A7CJpCwDb6eHz2vPqmWeeISYIC51sUA/v00M0AQAAYgHbyLGN7U8gdpG0BYDtdDI17cGqPaMSoUoUrUv7A2vfXK1Y1p5kAAAAsYBt5NjF9icQ20jaAgAAAAAAAEAUCbwRHwAAAAAAAAAg7EjaAgAAAAAAAEAUSZEYobNF6iyMubm5Tc5eDwAAgMhxOp1S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xNcEzt8JNLUfnkpEREREFAyOHDmCXbt2Wfdwp/DltdBWdoebN2+eCmaln61U0L766qsYM2aMurxVq1b46quvMHnyZHTo0EEFuzNmzGAlBhEREQVOSa7peB6Q2BCIryVlo/yrEBEREVHQtkZo37490tPTA704FKztEWTYhJmEtfPnzy/3+iNHjlQ/REREREFVadv8EqDZRUATfk4hIiIiouC1bt06ddi9e/dALwqFSk9bIiIiopAMbaOigQFfsLqWiIiIiILeli1b1GG7du0CvSjkY5ywRURERJGptBgozgVikhnYEhEREVFI2Lp1qzps27ZtoBeFfIyVtkRERBR5CjOAr2sax2t0CPTSEBERERG5Zd++feqwadOmXGNhjpW2REREFHkKDtuOn/JxIJeEiIiIiMhtR48eVYd169blWgtzDG2JiIgo8hRlGYf9PwTq9A300hARERERVaqsrAxHjhxRxxnahj+GtkRERBR5irONw7r9Ar0kRERERERuycnJQWFhoTpep04drrUwx9CWiIiIIrfSNjY10EtCREREROQWXWWbmJiI5ORkrrUwx9CWiIiIIrfSNi4t0EtCRERERORRP1upso2KiuJaC3MMbYmIiChyQ9uYlEAvCRERERGRW9jPNrIwtCUiIqLIbI8QkwxExwR6SYiIiIiIPK60pfDH0JaIiIgis9I2jv1siYiIiCj0Km0Z2kYGhrZEREQUeYqzOISMiIiIiEKy0rZu3bqBXhTyA4a2REREFHmKsoFYDiEjIiIiotBQVlaGQ4cOqeMMbSNDbKAXgIiIiCgglbZsj0BEREREISAjIwM9evTAjh071OnmzZsHepHID1hpS0RERJHZ05aVtkREREQUApYsWWINbEXLli0DujzkHwxtiYiIKELbI3AQGREREREFP3NgK1q1ahWwZSH/YWhLREREkYftEYiIiIgoRGzfvt16/Pbbb0fr1q0DujzkHwxtiYiIKPKwPQIRERERhYiDBw+qw6effhqTJ09GVFRUoBeJ/IChLREREUWeoiy2RyAiIiKikJCTk6MOU1PZ3iuSMLQlIiKiyKy0jUsL9FIQEREREbkd2iYnJ3NtRRCGtkRERBR5WGlLRERERCEW2qakpAR6UciPGNoSERFRZCktAkoL2B6BiIiIiEJCbm6uOmRoG1kY2hIREVFkKTYqFdgegYiIiIhCAdsjRCaGtkRERBR5rRFELAc5EBEREVHwY3uEyMTQloiIiCJvCJngIDIiIiIiCgFsjxCZGNoSERFRZGGlLRERERGFEFbaRiaGtkRERBRZWGlLFoWFhejcuTPmzZtnXSd33303oqKi7H5ee+016+VTp05Fy5YtkZycjFGjRuHgwYNcn0REROQzpaWl1kpb+fxBkYOhLREREUWWohPGYWxaoJeEAqigoABXXnkl1q9fb3f+unXr8PTTT+Pw4cPWnxtuuEFdtmTJEtx444148cUXsWrVKhX6XnPNNQH6DYiIiCgSyAbisrIyREdHo27duoFeHPKjWH8+GBEREVHAZW01hpAl8ENvpJJgVgJbVyTEve2221x+KZoyZQrGjBmDCy+8UJ1+++230apVK2zbtg2tW7f2+XITERFR5Nm9e7c6bNy4MWJjGeNFElbaEhERUWTJ2giktQeiogK9JBQg8+fPx9ChQ/HHH3849YvbtWsX2rVr5/J2ixcvxqBBg6ynW7RogSZNmqgKXCIiIiJvkuralStXYvPmzep0s2bNuIIjDCN6IiIiiiyZm4AaHQK9FBRA48aNc3n+hg0b1Bek119/HTNnzkTt2rVVj9srrrhCXb5nzx4V0ppJ1cvevXvLbcEgP1pmZqa1N538+Jo8hvw+/nisUMb1xPXE5xJfd8GI701cT9JH/9prr7X7zFGV/+l8LlVtXQXD5yeGtkRERBR5lbYNhwV6KSgISWgbExOjKlm+/fZbVYl7/fXXq6Ef559/PvLz85GQkGB3Gzmdl5fn8v4mTZqEiRMnOp0vfXLlvnxNvmxkZGRY++AR1xOfT3zNBQu+P3Ed8blUucmTJ9udTk1NxaFDh/h689N7k+yBFWgMbYmIiChyFJ4A8g8Z7RGIHEif29GjRyMtzRhS161bN9XjVnrZSmibmJjoFLZKJW1SUpLLdTlhwgSMHz/ertJWAuF69eqhRo0afvniERUVpR6PoS3XE59PCNnX3M6dO7Fp0yaceeaZCBd8f+I64nOpcm3btsXff/9tPd20aVPUr1+frzc/vTdlZ2cj0BjaEhERUWS1RhBsj0Dl0IGt1rFjR8ydO1cdl9YI+/fvt7tcWiM4tkwwV+E6VuYKCXP8FaLKFw9/Pl6o4nriegrm55IedDhv3jwMGTIE4YKvO64jPpcq5tjDtjobhPh6C811FfglICIiIvJnawTBSlty4fHHH3cKRFasWIFOnTqp4/3798eCBQusl23fvl2FtnI+EZGvOQ5PJKLw5thTtVatWgFbFgoMhrZEREQUObI2A0mNgLjUQC8JBaFzzz0XCxcuxAsvvKB2R37nnXfw8ccf47777lOX33TTTfjss88wbdo0bNu2DbfccguGDx+OVq1aBXrRiShMSV9FzVXlPhGFL8eWTA0bNgzYslBgMLQlIiKiyJG7G0huHuiloCDVu3dvTJ8+XQW10hbhxRdfVCFt37591eUDBgxQ/W3vuusudOnSBXFxcfjwww8DvdhEFKZyc3Oxdu1a62mGtkSRGdqOGDECTzzxBM4444xALxL5GXvaEhERUeTI3Qcku+4/SpHJXMUmRo0apX7KM3bsWPVDRORrJ598sl1oW1JSwpVOFEHy8vLU4VlnnYU77rgj0ItDAcBKWyIiIooceXuBJIa2REQU/MyBrdCTzL/66itcdtllyMnJCdCSEZE/K20TExO5wiMUK22JiIgocuTtN3raEhERhRgd2l566aXqsHnz5njuuecCvFRE5CsMbYmVtkRERBQZZDf4ogwgnpN3iYgouBUVFTmdl5WVZXf6zz//9OMSEZG/MbQlhrZEREQUGUpygbISIC490EtCRERUoYyMjHIrbbVt27ZxLRKFgTVr1mDgwIGYM2eOy562SUlJAVoyCjSGtkRERBQZCi1fgONqBHpJiIiIKnTixIlKzztw4IDTMEUiCj0vv/yyqpw/88wz7V7TrLQlhrZEREQUGaQ1gmClLRERhVCl7QcffKAOjx07huLiYqfglohCW3R0tMuNM7q6Pjk5OSDLRYHH0JaIiIgiQ1GmcRjP9ghERBTcJKAVXbp0QbNmzaznObZI+OeffwKyfETkPTExMdbje/bssR4/fvy4OqxduzZXd4SqcmhbWFiIzp07Y968eXZPqCuuuAJpaWlo2rQpXn31Vbvb/Pzzz+jUqZPqxzF48GBs2rSpektPRERE5HGlLdsjEBFRcNu5c6c6bN68OerUqaOOHz161GkY2apVqwKyfETkPTk5OU6hbWlpKUNbqlpoW1BQgCuvvBLr16+3O1/Ok38uf/31F95++23cd999mD17trps165duOCCC3DHHXeo27Vq1Qrnn3++eiISERER+RzbIxARUYjYvn27OpTvzbrKTkJbcxWeOHz4cECW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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "收益率验证 (拆股日前后):\n", + "拆股日期: 2021-02-25\n", + "未复权收益率: -0.5063\n", + "前复权收益率: -0.0126\n", + "真实收益率: -0.0126\n" + ] + } + ], + "source": [ + "def generate_raw_stock_data(n_days=1000, seed=42):\n", + " \"\"\"生成包含拆股和分红事件的原始股票数据\"\"\"\n", + " np.random.seed(seed)\n", + " dates = pd.bdate_range(start='2020-01-02', periods=n_days)\n", + "\n", + " # 生成\"真实\"价格序列 (几何布朗运动)\n", + " mu, sigma = 0.12, 0.25\n", + " dt = 1 / 252\n", + " log_ret = (mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * np.random.randn(n_days)\n", + " true_prices = 100 * np.exp(np.cumsum(log_ret))\n", + "\n", + " # 模拟成交量\n", + " volume = np.random.lognormal(mean=15, sigma=0.5, size=n_days).astype(int)\n", + "\n", + " # 构建DataFrame\n", + " df = pd.DataFrame({\n", + " 'date': dates,\n", + " 'close': true_prices,\n", + " 'volume': volume\n", + " }).set_index('date')\n", + "\n", + " # 添加开高低价\n", + " daily_range = df['close'] * np.abs(np.random.randn(n_days)) * 0.02\n", + " df['open'] = df['close'] + np.random.randn(n_days) * daily_range * 0.5\n", + " df['high'] = np.maximum(df['close'], df['open']) + np.abs(np.random.randn(n_days)) * daily_range * 0.3\n", + " df['low'] = np.minimum(df['close'], df['open']) - np.abs(np.random.randn(n_days)) * daily_range * 0.3\n", + "\n", + " # 定义公司行为事件\n", + " events = []\n", + "\n", + " # 1:2拆股, 在第300个交易日\n", + " split_idx = 300\n", + " split_date = dates[split_idx]\n", + " events.append({'date': split_date, 'type': 'split', 'ratio': 2})\n", + "\n", + " # 现金分红, 每季度一次\n", + " for q in [60, 185, 310, 435, 560, 685, 810, 935]:\n", + " if q < n_days:\n", + " events.append({'date': dates[q], 'type': 'dividend', 'amount': 0.5})\n", + "\n", + " # 应用公司行为到\"未复权\"价格\n", + " unadj_close = df['close'].copy()\n", + " adj_factors = pd.Series(1.0, index=dates)\n", + "\n", + " print('-------adj factors------')\n", + " print(adj_factors)\n", + " print('-------end of adj factors------')\n", + "\n", + "\n", + " for event in sorted(events, key=lambda x: x['date']):\n", + " idx = dates.get_loc(event['date'])\n", + " if event['type'] == 'split':\n", + " # 拆股\n", + " unadj_close.iloc[idx:] = unadj_close.iloc[idx:] / event['ratio']\n", + " adj_factors.iloc[idx:] = adj_factors.iloc[idx:] * event['ratio']\n", + " elif event['type'] == 'dividend':\n", + " # 分红\n", + " price_before = unadj_close.iloc[idx - 1] if idx > 0 else unadj_close.iloc[idx]\n", + " div_ratio = 1 - event['amount'] / price_before\n", + " unadj_close.iloc[idx:] = unadj_close.iloc[idx:] * div_ratio\n", + " adj_factors.iloc[:idx] = adj_factors.iloc[:idx] * div_ratio\n", + "\n", + " df['unadj_close'] = unadj_close\n", + " df['adj_factor'] = adj_factors\n", + "\n", + " events_df = pd.DataFrame(events)\n", + "\n", + " return df, events_df\n", + "\n", + "def forward_adjust(unadj_prices, adj_factor):\n", + " \"\"\"前复权:以最新价格为基准,向前调整历史价格\"\"\"\n", + " normalized_factor = adj_factor / adj_factor.iloc[-1]\n", + " return unadj_prices * normalized_factor\n", + "\n", + "def backward_adjust(unadj_prices, adj_factor):\n", + " \"\"\"后复权:以最早价格为基准,向后调整\"\"\"\n", + " normalized_factor = adj_factor / adj_factor.iloc[0]\n", + " return unadj_prices * normalized_factor\n", + "\n", + "# 生成数据\n", + "stock_data, events = generate_raw_stock_data()\n", + "\n", + "print(\"公司行为事件:\")\n", + "print(events.to_string(index=False))\n", + "print(f\"\\n数据形状: {stock_data.shape}\")\n", + "\n", + "# 计算复权价格\n", + "stock_data['fwd_adj_close'] = forward_adjust(stock_data['unadj_close'], stock_data['adj_factor'])\n", + "stock_data['bwd_adj_close'] = backward_adjust(stock_data['unadj_close'], stock_data['adj_factor'])\n", + "\n", + "# 可视化对比\n", + "fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n", + "\n", + "# 1. 未复权价格\n", + "ax = axes[0, 0]\n", + "ax.plot(stock_data['unadj_close'], color='blue', linewidth=1)\n", + "ax.set_title('未复权价格 (可见拆股/分红跳跃)', fontsize=13)\n", + "ax.grid(alpha=0.3)\n", + "\n", + "for _, event in events.iterrows():\n", + " ax.axvline(x=event['date'], color='red', linestyle='--', alpha=0.5)\n", + "\n", + "# 2. 前复权价格\n", + "ax = axes[0, 1]\n", + "ax.plot(stock_data['fwd_adj_close'], color='green', linewidth=1)\n", + "ax.set_title('前复权价格 (当前价格真实)', fontsize=13)\n", + "ax.grid(alpha=0.3)\n", + "\n", + "# 3. 后复权价格\n", + "ax = axes[1, 0]\n", + "ax.plot(stock_data['bwd_adj_close'], color='orange', linewidth=1)\n", + "ax.set_title('后复权价格 (历史价格真实)', fontsize=13)\n", + "ax.grid(alpha=0.3)\n", + "\n", + "# 4. \"真实\"价格对比\n", + "ax = axes[1, 1]\n", + "ax.plot(stock_data['close'], label='真实连续价格', color='black', linewidth=1.5)\n", + "ax.plot(stock_data['fwd_adj_close'], label='前复权价格', color='green', linewidth=1, alpha=0.7)\n", + "ax.set_title('真实价格 vs 前复权价格', fontsize=13)\n", + "ax.legend()\n", + "ax.grid(alpha=0.3)\n", + "\n", + "plt.suptitle('复权价格对比', fontsize=15, y=1.02)\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# 验证收益率\n", + "print(\"\\n收益率验证 (拆股日前后):\")\n", + "split_date = events[events['type'] == 'split']['date'].iloc[0]\n", + "split_idx = stock_data.index.get_loc(split_date)\n", + "\n", + "print(f\"拆股日期: {split_date.strftime('%Y-%m-%d')}\")\n", + "print(f\"未复权收益率: {stock_data['unadj_close'].iloc[split_idx] / stock_data['unadj_close'].iloc[split_idx-1] - 1:.4f}\")\n", + "print(f\"前复权收益率: {stock_data['fwd_adj_close'].iloc[split_idx] / stock_data['fwd_adj_close'].iloc[split_idx-1] - 1:.4f}\")\n", + "print(f\"真实收益率: {stock_data['close'].iloc[split_idx] / stock_data['close'].iloc[split_idx-1] - 1:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "632a08a5", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# TOPIC 2: Simple Returns vs Log Returns (简单收益率 vs 对数收益率)\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# Two ways to measure how much a price changed:\n", + "# - Simple return: r = (P_t / P_{t-1}) - 1\n", + "# - Log return: r = ln(P_t / P_{t-1})\n", + "#\n", + "# Why log returns are preferred in quant research:\n", + "# - Time-additive: sum daily log returns to get total log return\n", + "# - Better statistical properties (closer to normal distribution)\n", + "# - Numerically stable for long compounding periods\n", + "#\n", + "# At daily frequency the difference is tiny (~1bp), but it grows at\n", + "# monthly/annual frequency — never mix the two in the same calculation.\n", + "\n", + "# =============================================================================\n", + "\n", + "# 使用前复权价格计算收益率" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50ad6628", + "metadata": {}, + "outputs": [], + "source": [ + "prices = stock_data['fwd_adj_close']\n", + "\n", + "# 简单收益率\n", + "simple_returns = prices.pct_change().dropna()\n", + "\n", + "# 对数收益率\n", + "log_returns = np.log(prices / prices.shift(1)).dropna()\n", + "\n", + "# 对比\n", + "comparison = pd.DataFrame({\n", + " '简单收益率': simple_returns,\n", + " '对数收益率': log_returns,\n", + " '差异': simple_returns - log_returns,\n", + " '差异(bp)': (simple_returns - log_returns) * 10000 # 基点\n", + "})\n", + "\n", + "print(\"简单收益率 vs 对数收益率统计:\")\n", + "print(f\"平均绝对差异: {comparison['差异'].abs().mean():.6f} ({comparison['差异(bp)'].abs().mean():.6f})\")\n", + "print(f\"最大差异: {comparison['差异'].abs().max():.6f} ({comparison['差异(bp)'].abs().max():.6f})\")\n", + "print(f\"\\n日频数据下两者差异很小,但月频或年频时差异会显著增大。\")\n", + "\n", + "# 验证时间可加性\n", + "print(\"\\n=== 时间可加性验证 ===\")\n", + "# 对数收益率:时间上直接求和\n", + "total_log_return = log_returns.sum()\n", + "actual_total = np.log(prices.iloc[-1] / prices.iloc[0])\n", + "print(f\"对数收益率求和: {total_log_return:.6f}\")\n", + "print(f\"实际总对数收益: {actual_total:.6f}\")\n", + "print(f\"差异: {abs(total_log_return - actual_total):.10f} (几乎为零)\")\n", + "\n", + "print(\"\\n简单收益率需要连乘:\")\n", + "total_simple_product = (1 + simple_returns).prod() - 1\n", + "actual_simple = prices.iloc[-1] / prices.iloc[0] - 1\n", + "print(f\"简单收益率连乘: {total_simple_product:.6f}\")\n", + "print(f\"实际总简单收益: {actual_simple:.6f}\")\n", + "print(f\"差异: {abs(total_simple_product - actual_simple):.10f}\")\n", + "\n", + "# 可视化\n", + "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n", + "\n", + "ax = axes[0]\n", + "ax.scatter(simple_returns, log_returns, alpha=0.3, s=5)\n", + "ax.plot([-0.1, 0.1], [-0.1, 0.1], 'r--', linewidth=1, label='y=x')\n", + "ax.set_xlabel('简单收益率')\n", + "ax.set_ylabel('对数收益率')\n", + "ax.set_title('简单收益率 vs 对数收益率 (日频) ', fontsize=13)\n", + "ax.legend()\n", + "ax.grid(alpha=0.3)\n", + "\n", + "ax = axes[1]\n", + "# 月频对比\n", + "monthly_simple = prices.resample('ME').last().pct_change().dropna()\n", + "monthly_log = np.log(prices.resample('ME').last() / prices.resample('ME').last().shift(1)).dropna()\n", + "ax.scatter(monthly_simple, monthly_log, alpha=0.5, s=20)\n", + "ax.plot([-0.2, 0.2], [-0.2, 0.2], 'r--', linewidth=1, label='y=x')\n", + "ax.set_xlabel('简单收益率')\n", + "ax.set_ylabel('对数收益率')\n", + "ax.set_title('简单收益率 vs 对数收益率 (月频, 差异更明显) ', fontsize=13)\n", + "ax.legend()\n", + "ax.grid(alpha=0.3)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "a171952c", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# TOPIC 3: Multi-stock Panel & Missing Value Handling (多标的面板与缺失值处理)\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# Real market data panels have two main sources of missing values:\n", + "# - Suspensions (停牌): a stock halts trading for days/weeks\n", + "# - Data feed gaps: vendor errors, holidays, late reporting\n", + "#\n", + "# Strategies (choice depends on context):\n", + "# - ffill (forward fill): use last known price — standard for suspensions\n", + "# - Linear interpolation: smooth gaps — OK for short random gaps\n", + "# - ffill with limit: ffill but cap at N days — conservative choice\n", + "# - Drop: remove rows/columns — loses cross-sectional data\n", + "#\n", + "# Rule of thumb: if missing% > 10%, consider excluding the stock entirely.\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba3c95d7", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_multi_stock_panel(n_stocks=5, n_days=500, seed=42):\n", + " \"\"\"生成多标的日线数据面板,包含停牌和缺失值\"\"\"\n", + " np.random.seed(seed)\n", + " dates = pd.bdate_range(start='2022-01-03', periods=n_days)\n", + " \n", + " stock_names = [f'Stock_{chr(65+i)}' for i in range(n_stocks)] # Stock_A, Stock_B, ...\n", + " \n", + " all_data = {}\n", + " for i, name in enumerate(stock_names):\n", + " mu = np.random.uniform(0.05, 0.20)\n", + " sigma = np.random.uniform(0.15, 0.35)\n", + " S0 = np.random.uniform(20, 200)\n", + " \n", + " dt = 1 / 252\n", + " log_ret = (mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * np.random.randn(n_days)\n", + " prices = S0 * np.exp(np.cumsum(log_ret))\n", + " volume = np.random.lognormal(mean=14, sigma=0.8, size=n_days).astype(int)\n", + " \n", + " # 模拟停牌 (随机选择一些连续日期设为NaN)\n", + " suspension_starts = np.random.choice(range(50, n_days - 30), size=2 + i % 3, replace=False)\n", + " for start in suspension_starts:\n", + " duration = np.random.randint(1, 10)\n", + " end = min(start + duration, n_days)\n", + " prices[start:end] = np.nan\n", + " volume[start:end] = 0\n", + " \n", + " # 模拟随机缺失\n", + " random_missing = np.random.choice(range(n_days), size=5, replace=False)\n", + " prices[random_missing] = np.nan\n", + " \n", + " all_data[name] = pd.DataFrame({\n", + " 'close': prices,\n", + " 'volume': volume\n", + " }, index=dates)\n", + " \n", + " # 构建面板\n", + " price_panel = pd.DataFrame({name: data['close'] for name, data in all_data.items()})\n", + " volume_panel = pd.DataFrame({name: data['volume'] for name, data in all_data.items()})\n", + " \n", + " return price_panel, volume_panel\n", + "\n", + "# 生成多标的面板\n", + "price_panel, volume_panel = generate_multi_stock_panel(n_stocks=5, n_days=500)\n", + "\n", + "# 缺失值统计\n", + "missing_stats = pd.DataFrame({\n", + " '总天数': len(price_panel),\n", + " '缺失天数': price_panel.isna().sum(),\n", + " '缺失比例': price_panel.isna().mean(),\n", + " '最长连续缺失': [price_panel[col].isna().astype(int).groupby(\n", + " price_panel[col].notna().astype(int).cumsum()).sum().max()\n", + " for col in price_panel.columns]\n", + "})\n", + "\n", + "print(\"缺失值统计:\")\n", + "print(missing_stats)\n", + "print(f\"\\n面板形状: {price_panel.shape}\")\n", + "\n", + "# 缺失值处理方法对比\n", + "def handle_missing_values(prices, method='ffill'):\n", + " \"\"\"处理缺失值的不同方法\"\"\"\n", + " if method == 'ffill':\n", + " # ffill = forward fill (前向填充): 用前一个有效值填补缺失值\n", + " # 最常用于停牌场景: 假设停牌期间价格保持不变\n", + " return prices.ffill()\n", + " elif method == 'linear':\n", + " # 线性插值\n", + " return prices.interpolate(method='linear')\n", + " elif method == 'drop':\n", + " # 直接删除\n", + " return prices.dropna()\n", + " elif method == 'ffill_limit':\n", + " # 有限制的向前填充 (最多填充5天)\n", + " return prices.ffill(limit=5)\n", + " else:\n", + " raise ValueError(f\"Unknown method: {method}\")\n", + "\n", + "# 对Stock_A展示不同方法的效果\n", + "stock_a = price_panel['Stock_A'].copy()\n", + "\n", + "fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n", + "\n", + "methods = {\n", + " '原始数据 (含缺失值) ': stock_a,\n", + " '向前填充 (ffill)': handle_missing_values(stock_a, 'ffill'),\n", + " '线性插值 (interpolate)': handle_missing_values(stock_a, 'linear'),\n", + " '有限填充 (ffill, limit=5)': handle_missing_values(stock_a, 'ffill_limit')\n", + "}\n", + "\n", + "for ax, (title, data) in zip(axes.flat, methods.items()):\n", + " ax.plot(data, linewidth=1)\n", + " # 标记原始缺失位置\n", + " missing_mask = stock_a.isna()\n", + " if not missing_mask.all() and title != '原始数据 (含缺失值) ':\n", + " filled_values = data[missing_mask].dropna()\n", + " if len(filled_values) > 0:\n", + " ax.scatter(filled_values.index, filled_values.values,\n", + " color='red', s=15, zorder=5, label='填充值')\n", + " ax.legend(fontsize=9)\n", + " ax.set_title(title, fontsize=12)\n", + " ax.grid(alpha=0.3)\n", + "\n", + "plt.suptitle('缺失值处理方法对比 (Stock_A)', fontsize=14, y=1.02)\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "print(\"\\n实践建议:\")\n", + "print(\"- 停牌: 使用ffill (假设停牌期间价格不变) \")\n", + "print(\"- 短期缺失(<3天): 可用线性插值\")\n", + "print(\"- 长期缺失(>10天): 考虑从分析中排除该标的\")\n", + "print(\"- 计算收益率前: 先填充缺失值, 再计算\")\n", + "\n", + "\n", + "# 构建干净的多标的数据面板\n", + "# Steps: ffill → drop stocks/rows still NaN → compute returns\n", + "#\n", + "# ⚠️ Look-Ahead Bias Warning: DO NOT use bfill() here.\n", + "# bfill() fills early NaN rows by looking at future prices, which means\n", + "# your \"day 1\" price would be sourced from a future observation.\n", + "# This is data leakage — it would inflate backtest performance.\n", + "# Safe rule: only ffill (past → present), then drop what remains NaN.\n", + "\n", + "clean_prices = price_panel.ffill() # only look backward\n", + "clean_prices = clean_prices.dropna(how='any') # drop rows where any stock still NaN\n", + "\n", + "print(f\"清洗前缺失值: {price_panel.isna().sum().sum()}\")\n", + "print(f\"清洗后缺失值: {clean_prices.isna().sum().sum()}\")\n", + "print(f\"(dropped {len(price_panel) - len(clean_prices)} leading rows to avoid look-ahead bias)\")\n", + "\n", + "# 计算收益率面板\n", + "returns_panel = clean_prices.pct_change().dropna()\n", + "\n", + "# 可视化归一化价格\n", + "fig, ax = plt.subplots(figsize=(14, 6))\n", + "normalized = clean_prices / clean_prices.iloc[0] * 100\n", + "for col in normalized.columns:\n", + " ax.plot(normalized[col], label=col, linewidth=1.2)\n", + "\n", + "ax.set_title('多标的归一化价格走势 (基期=100) ', fontsize=14)\n", + "ax.legend()\n", + "ax.grid(alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# 相关性矩阵\n", + "corr_matrix = returns_panel.corr()\n", + "fig, ax = plt.subplots(figsize=(8, 6))\n", + "im = ax.imshow(corr_matrix.values, cmap='RdBu_r', vmin=-1, vmax=1)\n", + "ax.set_xticks(range(len(corr_matrix)))\n", + "ax.set_xticklabels(corr_matrix.columns, rotation=45)\n", + "ax.set_yticks(range(len(corr_matrix)))\n", + "ax.set_yticklabels(corr_matrix.columns)\n", + "for i in range(len(corr_matrix)):\n", + " for j in range(len(corr_matrix)):\n", + " ax.text(j, i, f'{corr_matrix.iloc[i,j]:.2f}', ha='center', va='center', fontsize=11)\n", + "plt.colorbar(im, ax=ax)\n", + "ax.set_title('收益率相关性矩阵', fontsize=14)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "689e9f4b", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# TOPIC 4: Outlier Detection & Treatment (异常值检测与处理)\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# Financial return series contain extreme values from real events (crashes,\n", + "# halts, data errors). How you handle them affects every downstream signal.\n", + "#\n", + "# Detection methods:\n", + "# - Z-score: |z| = |(x - mean) / std| > 3 — simple, but mean/std are\n", + "# themselves pulled by outliers (not robust)\n", + "# - MAD: uses median instead of mean — robust to extreme values,\n", + "# preferred for financial data\n", + "#\n", + "# Treatment methods:\n", + "# - Remove: delete the outlier row — reduces sample size\n", + "# - Winsorize: clip to [p1, p99] boundary — keeps all data, limits impact\n", + "# standard practice before factor construction\n", + "#\n", + "# Q-Q plot: visualizes how far returns deviate from a normal distribution.\n", + "# Fat tails (leptokurtosis) are a universal property of financial returns.\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8fd49375", + "metadata": {}, + "outputs": [], + "source": [ + "def detect_outliers_zscore(series, threshold=3.0):\n", + " \"\"\"Z-score (标准分数) 方法检测异常值\n", + " \n", + " Z = (x - mean) / std\n", + " 即:这个值偏离平均值多少个标准差?\n", + " |Z| > threshold (默认3) 视为异常值\n", + " \n", + " 通俗理解:如果大家平均考70分,标准差10分,你考了100分,\n", + " Z = (100-70)/10 = 3,说明你偏离平均值3个标准差,属于异常值。\n", + " \n", + " 缺点:均值和标准差本身受异常值影响\n", + " \"\"\"\n", + " z_scores = (series - series.mean()) / series.std()\n", + " outlier_mask = z_scores.abs() > threshold\n", + " return outlier_mask, z_scores\n", + "\n", + "\n", + "def detect_outliers_mad(series, threshold=3.0):\n", + " \"\"\"MAD (中位数绝对偏差, Median Absolute Deviation) 方法检测异常值\n", + " \n", + " MAD = median(|x - median(x)|) 即所有值与中位数的距离的中位数\n", + " Modified Z = 0.6745 * (x - median) / MAD\n", + " \n", + " 优点:用中位数替代均值,比Z-score更稳健,不受极端值影响\n", + " 通俗理解:Z-score用\"平均值\"做参考,容易被极端值带偏;\n", + " MAD用\"中位数\"做参考,即使有极端值也不受影响。\n", + " \"\"\"\n", + " median = series.median()\n", + " mad = (series - median).abs().median()\n", + " if mad == 0:\n", + " mad = 1e-10 # 避免除零\n", + " modified_z = 0.6745 * (series - median) / mad\n", + " outlier_mask = modified_z.abs() > threshold\n", + " return outlier_mask, modified_z\n", + "\n", + "\n", + "def winsorize(series, lower_pct=0.01, upper_pct=0.99):\n", + " \"\"\"Winsorize (缩尾处理): 将超出分位数的值截断到分位数边界\n", + " \n", + " 不同于删除异常值,Winsorize保留了所有数据点,只是限制了极端值。\n", + " 比如设 lower_pct=0.01, upper_pct=0.99:\n", + " - 低于1%分位数的值 -> 拉回到1%分位数\n", + " - 高于99%分位数的值 -> 拉回到99%分位数\n", + " \"\"\"\n", + " lower = series.quantile(lower_pct)\n", + " upper = series.quantile(upper_pct)\n", + " return series.clip(lower=lower, upper=upper)\n", + "\n", + "\n", + "# 使用Stock_A的收益率进行演示\n", + "ret_a = returns_panel['Stock_A'].copy()\n", + "\n", + "# 人为注入一些异常值以演示效果\n", + "np.random.seed(123)\n", + "outlier_indices = np.random.choice(ret_a.index, size=5, replace=False)\n", + "ret_a_with_outliers = ret_a.copy()\n", + "for idx in outlier_indices:\n", + " ret_a_with_outliers[idx] = np.random.choice([-1, 1]) * np.random.uniform(0.15, 0.30)\n", + "\n", + "# 检测异常值\n", + "zscore_mask, z_scores = detect_outliers_zscore(ret_a_with_outliers, threshold=3.0)\n", + "mad_mask, mad_scores = detect_outliers_mad(ret_a_with_outliers, threshold=3.0)\n", + "\n", + "print(\"异常值检测结果:\")\n", + "print(f\"Z-score方法 (|Z|>3): 检测到 {zscore_mask.sum()} 个异常值\")\n", + "print(f\"MAD方法 (|Modified Z|>3): 检测到 {mad_mask.sum()} 个异常值\")\n", + "\n", + "# Winsorize处理\n", + "ret_winsorized = winsorize(ret_a_with_outliers, lower_pct=0.01, upper_pct=0.99)\n", + "\n", + "print(f\"\\nWinsorize前 - 范围: [{ret_a_with_outliers.min():.4f}, {ret_a_with_outliers.max():.4f}]\")\n", + "print(f\"Winsorize后 - 范围: [{ret_winsorized.min():.4f}, {ret_winsorized.max():.4f}]\")\n", + "\n", + "\n", + "# 可视化异常值检测\n", + "fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n", + "\n", + "# 1. 收益率时序与异常值标记\n", + "ax = axes[0, 0]\n", + "ax.plot(ret_a_with_outliers, color='blue', linewidth=0.8, alpha=0.7)\n", + "outliers = ret_a_with_outliers[zscore_mask]\n", + "ax.scatter(outliers.index, outliers.values, color='red', s=40, zorder=5, label=f'异常值 ({len(outliers)}个)')\n", + "ax.set_title('Z-score异常值检测 (|Z|>3)', fontsize=13)\n", + "ax.legend()\n", + "ax.grid(alpha=0.3)\n", + "\n", + "# 2. MAD方法\n", + "ax = axes[0, 1]\n", + "ax.plot(ret_a_with_outliers, color='blue', linewidth=0.8, alpha=0.7)\n", + "outliers_mad = ret_a_with_outliers[mad_mask]\n", + "ax.scatter(outliers_mad.index, outliers_mad.values, color='orange', s=40, zorder=5, label=f'异常值 ({len(outliers_mad)}个)')\n", + "ax.set_title('MAD异常值检测 (|Modified Z|>3)', fontsize=13)\n", + "ax.legend()\n", + "ax.grid(alpha=0.3)\n", + "\n", + "# 3. Winsorize前后分布对比\n", + "ax = axes[1, 0]\n", + "ax.hist(ret_a_with_outliers, bins=60, alpha=0.5, label='原始', color='blue', density=True)\n", + "ax.hist(ret_winsorized, bins=60, alpha=0.5, label='Winsorize后', color='green', density=True)\n", + "ax.set_title('Winsorize前后收益率分布', fontsize=13)\n", + "ax.legend()\n", + "ax.grid(alpha=0.3)\n", + "\n", + "# 4. Q-Q图\n", + "ax = axes[1, 1]\n", + "sorted_data = np.sort(ret_a_with_outliers.dropna())\n", + "n = len(sorted_data)\n", + "theoretical = stats.norm.ppf(np.arange(1, n + 1) / (n + 1))\n", + "ax.scatter(theoretical, sorted_data, alpha=0.5, s=10)\n", + "ax.plot([theoretical.min(), theoretical.max()], \n", + " [theoretical.min() * ret_a_with_outliers.std() + ret_a_with_outliers.mean(),\n", + " theoretical.max() * ret_a_with_outliers.std() + ret_a_with_outliers.mean()],\n", + " 'r--', linewidth=1, label='正态分布参考线')\n", + "ax.set_xlabel('理论分位数')\n", + "ax.set_ylabel('样本分位数')\n", + "ax.set_title('Q-Q图 (检验正态性) ', fontsize=13)\n", + "ax.legend()\n", + "ax.grid(alpha=0.3)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "print(\"\\n方法选择建议:\")\n", + "print(\"- Z-score: 简单快速,但受极端值影响 (不够稳健) \")\n", + "print(\"- MAD: 更稳健,推荐用于金融数据\")\n", + "print(\"- Winsorize: 保留数据量,适合构建因子时使用\")\n", + "print(\"- 实际中常用: Winsorize(1%, 99%) + MAD检测\")" + ] + }, + { + "cell_type": "markdown", + "id": "e71339e7", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# TOPIC 4b: Circuit Breakers & Price Limit Flags (涨跌停标记)\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# In Chinese A-shares, stocks have a ±10% daily price limit (±5% for ST stocks).\n", + "# When a stock hits the limit, it is NOT a data error — it is real market data.\n", + "# However, it signals a critical data quality issue for downstream use:\n", + "#\n", + "# - The price IS the true closing price\n", + "# - But the stock may have been UNTRADEABLE for most of the day (zero liquidity)\n", + "# - A return of exactly +10% or -10% means you likely CANNOT execute at that price\n", + "#\n", + "# What to do in data prep (we flag, not remove):\n", + "# - Add a boolean column: `is_limit_up`, `is_limit_down`\n", + "# - Downstream strategy code can then decide to skip, weight-down, or\n", + "# exclude limit-hit days from signal calculations\n", + "#\n", + "# ⚠️ Survivorship Bias Note (out of scope for data prep, but important to know):\n", + "# This demo only generates stocks that \"survive\" the full period. In reality,\n", + "# stocks get delisted (bankruptcy, M&A, regulatory removal). If you only\n", + "# include currently-alive stocks in your historical dataset, you are\n", + "# implicitly selecting winners — your backtest returns will be inflated.\n", + "# Fix: source historical data that includes ALL stocks, including delisted ones.\n", + "# This is a DATA SOURCING problem, handled before data ever enters this pipeline.\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f31d3fff", + "metadata": {}, + "outputs": [], + "source": [ + "def flag_price_limits(prices, limit_pct=0.10):\n", + " \"\"\"Flag days where a stock hit the daily price limit (涨跌停).\n", + "\n", + " Returns a DataFrame of the same shape with values:\n", + " +1 = limit up (涨停)\n", + " -1 = limit down (跌停)\n", + " 0 = normal day\n", + "\n", + " A small tolerance (0.5%) is used because real closing prices may be\n", + " rounded and not land exactly on the theoretical limit price.\n", + " \"\"\"\n", + " returns = prices.pct_change()\n", + " tolerance = 0.005 # 0.5% buffer for rounding\n", + "\n", + " limit_up = (returns >= limit_pct - tolerance).astype(int)\n", + " limit_down = (returns <= -limit_pct + tolerance).astype(int) * -1\n", + " flags = limit_up + limit_down\n", + " return flags\n", + "\n", + "\n", + "# Inject realistic limit-hit events into the panel for demonstration\n", + "np.random.seed(99)\n", + "demo_prices = clean_prices.copy()\n", + "for col in demo_prices.columns:\n", + " # Randomly inject 3 limit-up and 3 limit-down days per stock\n", + " up_days = np.random.choice(range(1, len(demo_prices) - 1), size=3, replace=False)\n", + " down_days = np.random.choice(range(1, len(demo_prices) - 1), size=3, replace=False)\n", + " for d in up_days:\n", + " demo_prices.iloc[d][col] = demo_prices.iloc[d - 1][col] * 1.10 # exactly +10%\n", + " for d in down_days:\n", + " demo_prices.iloc[d][col] = demo_prices.iloc[d - 1][col] * 0.90 # exactly -10%\n", + "\n", + "limit_flags = flag_price_limits(demo_prices, limit_pct=0.10)\n", + "\n", + "# Summary\n", + "total_limit_up = (limit_flags == 1).sum().sum()\n", + "total_limit_down = (limit_flags == -1).sum().sum()\n", + "print(f\"\\n涨跌停统计:\")\n", + "print(f\" 涨停天数 (limit up): {total_limit_up}\")\n", + "print(f\" 跌停天数 (limit down): {total_limit_down}\")\n", + "print(f\"\\n各标的涨停次数:\")\n", + "print((limit_flags == 1).sum())\n", + "print(f\"\\n各标的跌停次数:\")\n", + "print((limit_flags == -1).sum())\n", + "\n", + "# Visualize limit flags on Stock_A\n", + "fig, axes = plt.subplots(2, 1, figsize=(14, 8), sharex=True)\n", + "\n", + "ax = axes[0]\n", + "ax.plot(demo_prices['Stock_A'], color='steelblue', linewidth=1, label='Stock_A Price')\n", + "limit_up_dates = limit_flags.index[limit_flags['Stock_A'] == 1]\n", + "limit_down_dates = limit_flags.index[limit_flags['Stock_A'] == -1]\n", + "ax.scatter(limit_up_dates, demo_prices.loc[limit_up_dates, 'Stock_A'],\n", + " color='red', s=60, zorder=5, label='涨停 (limit up)', marker='^')\n", + "ax.scatter(limit_down_dates, demo_prices.loc[limit_down_dates, 'Stock_A'],\n", + " color='green', s=60, zorder=5, label='跌停 (limit down)', marker='v')\n", + "ax.set_title('价格序列与涨跌停标记 (Stock_A)', fontsize=13)\n", + "ax.legend()\n", + "ax.grid(alpha=0.3)\n", + "\n", + "ax = axes[1]\n", + "ax.bar(limit_flags.index, limit_flags['Stock_A'],\n", + " color=limit_flags['Stock_A'].map({1: 'red', -1: 'green', 0: 'lightgray'}),\n", + " width=1)\n", + "ax.set_title('涨跌停标志 (+1=涨停, -1=跌停, 0=正常)', fontsize=13)\n", + "ax.set_ylim(-1.5, 1.5)\n", + "ax.set_yticks([-1, 0, 1])\n", + "ax.grid(alpha=0.3)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "print(\"\\n实践建议:\")\n", + "print(\"- 涨跌停日: 保留价格数据,但添加 is_limit_up / is_limit_down 标记列\")\n", + "print(\"- 策略层: 遇到涨跌停标记时跳过信号,因无法成交\")\n", + "print(\"- 统计分析: 计算波动率/相关性时可选择排除涨跌停日,避免失真\")" + ] + }, + { + "cell_type": "markdown", + "id": "32e68310", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# TOPIC 5: Trading Calendar & Cross-market Alignment (交易日历与跨市场对齐)\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# Different markets trade on different days (national holidays, weekends).\n", + "# When combining data from multiple markets, you must decide how to align\n", + "# the time axis — the wrong choice distorts correlation calculations.\n", + "#\n", + "# Two alignment strategies:\n", + "# - Intersection (取交集): keep only dates both markets are open.\n", + "# Best for: correlation, regression, signal comparison.\n", + "# Drawback: loses data on single-market trading days.\n", + "#\n", + "# - Forward fill (向前填充): fill non-trading days with last known price.\n", + "# Best for: portfolio construction, continuous NAV calculation.\n", + "# Drawback: introduces artificial zero-return days — inflates Sharpe,\n", + "# understates volatility. Must be accounted for in analysis.\n", + "#\n", + "# Note: this demo uses simplified hardcoded holiday lists. Production systems\n", + "# should use exchange_calendars or pandas_market_calendars libraries.\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc3eb282", + "metadata": {}, + "outputs": [], + "source": [ + "def create_trading_calendars():\n", + " \"\"\"创建不同市场的交易日历示例\n", + " \n", + " 不同市场有不同的假日和交易时间,跨市场策略必须正确处理。\n", + " \"\"\"\n", + " # 生成2024年的工作日\n", + " all_bdays = pd.bdate_range(start='2024-01-01', end='2024-12-31')\n", + " \n", + " # 美国市场假日 (简化版)\n", + " us_holidays = pd.to_datetime([\n", + " '2024-01-01', # 新年\n", + " '2024-01-15', # MLK Day\n", + " '2024-02-19', # Presidents Day\n", + " '2024-03-29', # Good Friday\n", + " '2024-05-27', # Memorial Day\n", + " '2024-06-19', # Juneteenth\n", + " '2024-07-04', # Independence Day\n", + " '2024-09-02', # Labor Day\n", + " '2024-11-28', # Thanksgiving\n", + " '2024-12-25', # Christmas\n", + " ])\n", + " \n", + " # 中国A股假日 (简化版)\n", + " cn_holidays = pd.to_datetime([\n", + " '2024-01-01', # 元旦\n", + " '2024-02-09', '2024-02-12', '2024-02-13', '2024-02-14',\n", + " '2024-02-15', '2024-02-16', # 春节\n", + " '2024-04-04', '2024-04-05', # 清明\n", + " '2024-05-01', '2024-05-02', '2024-05-03', # 劳动节\n", + " '2024-06-10', # 端午\n", + " '2024-09-16', '2024-09-17', # 中秋\n", + " '2024-10-01', '2024-10-02', '2024-10-03', '2024-10-04',\n", + " '2024-10-07', # 国庆\n", + " ])\n", + " \n", + " us_trading_days = all_bdays[~all_bdays.isin(us_holidays)]\n", + " cn_trading_days = all_bdays[~all_bdays.isin(cn_holidays)]\n", + " \n", + " return us_trading_days, cn_trading_days\n", + "\n", + "us_calendar, cn_calendar = create_trading_calendars()\n", + "\n", + "print(f\"2024年美股交易日: {len(us_calendar)}天\")\n", + "print(f\"2024年A股交易日: {len(cn_calendar)}天\")\n", + "print(f\"共同交易日: {len(us_calendar.intersection(cn_calendar))}天\")\n", + "print(f\"仅美股交易日: {len(us_calendar.difference(cn_calendar))}天\")\n", + "print(f\"仅A股交易日: {len(cn_calendar.difference(us_calendar))}天\")\n", + "\n", + "# 模拟跨市场数据对齐\n", + "np.random.seed(42)\n", + "\n", + "# 生成美股和A股数据\n", + "us_prices = pd.Series(\n", + " 100 * np.exp(np.cumsum(np.random.randn(len(us_calendar)) * 0.01)),\n", + " index=us_calendar, name='US_ETF'\n", + ")\n", + "cn_prices = pd.Series(\n", + " 50 * np.exp(np.cumsum(np.random.randn(len(cn_calendar)) * 0.015)),\n", + " index=cn_calendar, name='CN_ETF'\n", + ")\n", + "\n", + "# 直接合并会有大量NaN\n", + "combined_raw = pd.DataFrame({'US_ETF': us_prices, 'CN_ETF': cn_prices})\n", + "print(f\"直接合并后:\")\n", + "print(f\" 总行数: {len(combined_raw)}\")\n", + "print(f\" US_ETF缺失: {combined_raw['US_ETF'].isna().sum()}\")\n", + "print(f\" CN_ETF缺失: {combined_raw['CN_ETF'].isna().sum()}\")\n", + "\n", + "# 方法1: 取交集 (只保留双方都交易的日期)\n", + "common_dates = us_calendar.intersection(cn_calendar)\n", + "combined_inner = combined_raw.loc[common_dates].dropna()\n", + "\n", + "# 方法2: 向前填充 (用最近的收盘价填充)\n", + "combined_ffill = combined_raw.ffill().dropna()\n", + "\n", + "print(f\"\\n方法1 (取交集): {len(combined_inner)}行\")\n", + "print(f\"方法2 (ffill): {len(combined_ffill)}行\")\n", + "\n", + "# 可视化\n", + "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n", + "\n", + "ax = axes[0]\n", + "ax.plot(combined_inner.index, combined_inner['US_ETF'] / combined_inner['US_ETF'].iloc[0],\n", + " label='US_ETF', linewidth=1.2)\n", + "ax.plot(combined_inner.index, combined_inner['CN_ETF'] / combined_inner['CN_ETF'].iloc[0],\n", + " label='CN_ETF', linewidth=1.2)\n", + "ax.set_title('方法1: 取交集日期', fontsize=13)\n", + "ax.legend()\n", + "ax.grid(alpha=0.3)\n", + "\n", + "ax = axes[1]\n", + "ax.plot(combined_ffill.index, combined_ffill['US_ETF'] / combined_ffill['US_ETF'].iloc[0],\n", + " label='US_ETF', linewidth=1.2)\n", + "ax.plot(combined_ffill.index, combined_ffill['CN_ETF'] / combined_ffill['CN_ETF'].iloc[0],\n", + " label='CN_ETF', linewidth=1.2)\n", + "ax.set_title('方法2: 向前填充', fontsize=13)\n", + "ax.legend()\n", + "ax.grid(alpha=0.3)\n", + "\n", + "plt.suptitle('跨市场数据对齐方法对比', fontsize=14, y=1.02)\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "print(\"\\n实践建议:\")\n", + "print(\"- 计算相关性、回归: 使用交集日期,确保数据同步\")\n", + "print(\"- 构建组合/计算市值: 使用ffill,避免数据断裂\")\n", + "print(\"- 重要: ffill会引入假的'零收益日',计算波动率时需注意\")" + ] + }, + { + "cell_type": "markdown", + "id": "625e9a2a", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# TOPIC 6: End-to-End DataPipeline Class (完整数据清洗管道)\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# Combines all prior topics into a reusable, sequential pipeline:\n", + "# Step 1 — Filter stocks with too many missing values\n", + "# Step 2 — Fill remaining missing values (ffill only — no bfill/look-ahead)\n", + "# Step 3 — Compute return series\n", + "# Step 4 — Detect outliers (MAD) and apply Winsorize\n", + "# Step 5 — Generate a data quality report per stock\n", + "#\n", + "# This pattern (class-based pipeline with a .run() method) mirrors how\n", + "# production quant systems structure data ingestion — each step is auditable,\n", + "# configurable, and produces diagnostic output.\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab7f3e0b", + "metadata": {}, + "outputs": [], + "source": [ + "class DataPipeline:\n", + " \"\"\"简单的数据清洗管道\n", + " \n", + " 实现了从原始数据到干净数据面板的完整流程:\n", + " 1. 缺失值处理\n", + " 2. 异常值处理\n", + " 3. 收益率计算\n", + " 4. 数据质量报告\n", + " \"\"\"\n", + " \n", + " def __init__(self, prices, max_missing_pct=0.1, winsorize_pct=(0.01, 0.99)):\n", + " self.raw_prices = prices.copy()\n", + " self.max_missing_pct = max_missing_pct\n", + " self.winsorize_pct = winsorize_pct\n", + " self.report = {}\n", + " \n", + " def run(self):\n", + " \"\"\"运行完整的数据清洗管道\"\"\"\n", + " print(\"=\" * 60)\n", + " print(\"数据清洗管道运行中...\")\n", + " print(\"=\" * 60)\n", + " \n", + " # Step 1: 过滤缺失值过多的标的\n", + " missing_pct = self.raw_prices.isna().mean()\n", + " valid_cols = missing_pct[missing_pct <= self.max_missing_pct].index.tolist()\n", + " dropped_cols = missing_pct[missing_pct > self.max_missing_pct].index.tolist()\n", + " \n", + " print(f\"\\n[Step 1] 过滤缺失值 > {self.max_missing_pct:.0%} 的标的\")\n", + " print(f\" 保留: {len(valid_cols)}个标的, 删除: {len(dropped_cols)}个标的\")\n", + " if dropped_cols:\n", + " print(f\" 被删除的标的: {dropped_cols}\")\n", + " \n", + " prices = self.raw_prices[valid_cols].copy()\n", + " \n", + " # Step 2: 填充缺失值\n", + " # ⚠️ Only ffill (backward-looking). bfill would use future prices to\n", + " # fill early NaNs — that is look-ahead bias / data leakage.\n", + " # Any rows still NaN after ffill are dropped (typically only the\n", + " # very first row if the series starts with a missing value).\n", + " print(f\"\\n[Step 2] 填充缺失值 (ffill only, no bfill)\")\n", + " n_missing_before = prices.isna().sum().sum()\n", + " prices = prices.ffill().dropna(how='any')\n", + " n_missing_after = prices.isna().sum().sum()\n", + " print(f\" 填充前: {n_missing_before}个NaN -> 填充后: {n_missing_after}个NaN\")\n", + " \n", + " # Step 3: 计算收益率\n", + " print(f\"\\n[Step 3] 计算收益率\")\n", + " returns = prices.pct_change().iloc[1:] # 删除第一行NaN\n", + " print(f\" 收益率面板: {returns.shape[0]}天 x {returns.shape[1]}标的\")\n", + " \n", + " # Step 4: 检测和处理异常值\n", + " print(f\"\\n[Step 4] 异常值检测与Winsorize\")\n", + " n_outliers_total = 0\n", + " returns_clean = returns.copy()\n", + " for col in returns_clean.columns:\n", + " mask, _ = detect_outliers_mad(returns_clean[col], threshold=5.0)\n", + " n_outliers = mask.sum()\n", + " n_outliers_total += n_outliers\n", + " # Winsorize\n", + " returns_clean[col] = winsorize(returns_clean[col], \n", + " self.winsorize_pct[0], self.winsorize_pct[1])\n", + " \n", + " print(f\" 检测到 {n_outliers_total} 个异常值 (MAD, |Z|>5)\")\n", + " print(f\" 已进行Winsorize处理 ({self.winsorize_pct[0]:.0%}, {self.winsorize_pct[1]:.0%})\")\n", + " \n", + " # Step 5: 数据质量报告\n", + " print(f\"\\n[Step 5] 数据质量报告\")\n", + " quality = pd.DataFrame({\n", + " '标的': valid_cols,\n", + " '日均收益率(bp)': (returns_clean.mean() * 10000).round(2),\n", + " '日波动率(%)': (returns_clean.std() * 100).round(3),\n", + " '年化收益率(%)': (returns_clean.mean() * 252 * 100).round(2),\n", + " '年化波动率(%)': (returns_clean.std() * np.sqrt(252) * 100).round(2),\n", + " '偏度': returns_clean.skew().round(3),\n", + " '峰度': returns_clean.kurtosis().round(3),\n", + " }).set_index('标的')\n", + " print(quality)\n", + " \n", + " self.clean_prices = prices\n", + " self.clean_returns = returns_clean\n", + " self.quality_report = quality\n", + " \n", + " print(f\"\\n管道运行完成!\")\n", + " return returns_clean\n", + "\n", + "# 运行管道\n", + "pipeline = DataPipeline(price_panel, max_missing_pct=0.15)\n", + "clean_returns = pipeline.run()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:trading] *", + "language": "python", + "name": "conda-env-trading-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/quant_etf_rotation_demo.ipynb b/quant_etf_rotation_demo.ipynb new file mode 100644 index 0000000..618d68f --- /dev/null +++ b/quant_etf_rotation_demo.ipynb @@ -0,0 +1,994 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "2cf0ca5c", + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + "A 股行业 ETF 轮动策略 Demo\n", + "A-Share Sector ETF Rotation Strategy Demo\n", + "==========================================\n", + "\n", + "策略逻辑 (Strategy Logic):\n", + " 1. 相对动量轮动 (Relative Momentum Rotation)\n", + " - 每月对所有行业 ETF 计算过去 N 个月的动量得分\n", + " - 买入得分最高的前 K 个 ETF,等权持有\n", + " 2. 双动量策略 (Dual Momentum, Gary Antonacci)\n", + " - 绝对动量 (Absolute Momentum): 若某 ETF 的动量 < 0,切换至国债 ETF 避险\n", + " - 相对动量 (Relative Momentum): 在通过绝对动量过滤的 ETF 中,买入相对最强的\n", + " 3. 等权基准 (Equal Weight Benchmark)\n", + " - 始终等权持有全部行业 ETF,不做轮动\n", + "\n", + "成本模拟 (Cost Simulation):\n", + " - 印花税 (Stamp Duty): 卖出 0.1%\n", + " - 佣金 (Commission): 买卖各 0.03%(万三)\n", + " - 滑点 (Slippage): 单边 0.05%\n", + " - 每月换仓合计约 0.22% 双边成本\n", + "\n", + "作者: GitHub Copilot (教学用途)\n", + "数据: 合成模拟数据 (Synthetic data for demonstration)\n", + "\"\"\"\n", + "\n", + "# ── 标准库 ──────────────────────────────────────────────────────────────────\n", + "import warnings\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "# matplotlib 必须在 pyplot 之前设置后端 (must set backend before importing pyplot)\n", + "import matplotlib\n", + "matplotlib.use(\"Agg\") # 无界面后端 (headless backend)\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.gridspec as gridspec\n", + "from matplotlib.patches import FancyArrowPatch\n", + "import matplotlib.ticker as mtick\n", + "\n", + "# 设置随机种子,保证结果可复现 (fix random seed for reproducibility)\n", + "np.random.seed(42)" + ] + }, + { + "cell_type": "markdown", + "id": "a6555659", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════════════\n", + "# §0 ETF 池定义 & 合成价格数据生成\n", + "# ETF Universe Definition & Synthetic Price Data Generation\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════════════\n", + "\n", + "# ── A 股行业 ETF 池 (A-Share Sector ETF Universe) ───────────────────────────\n", + "# 这里用合成数据模拟,真实代码只需替换为 AKShare/Tushare 数据读取\n", + "# (Synthetic data here; replace with AKShare/Tushare data in production)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "269510d5", + "metadata": {}, + "outputs": [], + "source": [ + "ETF_UNIVERSE = {\n", + " # 代码 (Code) : (中文名称, 英文名称, 年化收益均值, 年化波动率, 与市场相关性)\n", + " \"159995\": (\"芯片ETF\", \"Chip ETF\", 0.18, 0.45, 0.75),\n", + " \"159819\": (\"人工智能ETF\",\"AI ETF\", 0.20, 0.48, 0.72),\n", + " \"516160\": (\"新能源ETF\", \"New Energy ETF\", 0.15, 0.42, 0.70),\n", + " \"159928\": (\"消费ETF\", \"Consumer ETF\", 0.10, 0.28, 0.65),\n", + " \"512170\": (\"医疗ETF\", \"Healthcare ETF\", 0.08, 0.32, 0.60),\n", + " \"512880\": (\"证券ETF\", \"Securities ETF\", 0.12, 0.38, 0.80),\n", + " # 国债 ETF 作为避险资产 (Bond ETF as safe-haven asset)\n", + " \"511010\": (\"国债ETF\", \"Bond ETF\", 0.03, 0.03, -0.10),\n", + "}\n", + "\n", + "# 分离行业 ETF 和国债 ETF (separate sector ETFs from bond ETF)\n", + "SECTOR_ETFS = [code for code in ETF_UNIVERSE if code != \"511010\"]\n", + "BOND_ETF = \"511010\"\n", + "ALL_ETFS = list(ETF_UNIVERSE.keys())\n", + "\n", + "# ── 生成合成价格序列 (Generate Synthetic Price Series) ───────────────────────\n", + "# 用因子模型生成相关价格序列,模拟真实行业之间的联动关系\n", + "# (Factor model to generate correlated price series, mimicking real sector co-movement)\n", + "\n", + "TRADING_DAYS = 252 # 年交易日数 (trading days per year)\n", + "SIM_YEARS = 6 # 模拟年数 (simulation years): 2019-2024\n", + "N_DAYS = TRADING_DAYS * SIM_YEARS\n", + "\n", + "# 市场公共因子 (market common factor) —— 模拟沪深300走势\n", + "# 牛市/熊市分段:模拟 2019-2021 牛市,2021-2022 熊市,2023-2024 震荡\n", + "market_factor = np.zeros(N_DAYS)\n", + "# 第一阶段:2019-2021 牛市 (Bull market phase)\n", + "bull1 = int(N_DAYS * 0.40)\n", + "market_factor[:bull1] = np.random.normal(0.0008, 0.012, bull1)\n", + "# 第二阶段:2021-2022 熊市 (Bear market phase)\n", + "bear1 = int(N_DAYS * 0.20)\n", + "market_factor[bull1:bull1+bear1] = np.random.normal(-0.0006, 0.018, bear1)\n", + "# 第三阶段:2023-2024 震荡 (Sideways phase)\n", + "market_factor[bull1+bear1:] = np.random.normal(0.0002, 0.014, N_DAYS - bull1 - bear1)\n", + "\n", + "# 生成每只 ETF 的日收益率 (generate daily returns for each ETF)\n", + "daily_returns = {}\n", + "for code, (cn_name, en_name, ann_ret, ann_vol, mkt_corr) in ETF_UNIVERSE.items():\n", + " daily_mu = ann_ret / TRADING_DAYS # 日均收益 (daily mean return)\n", + " daily_sig = ann_vol / np.sqrt(TRADING_DAYS) # 日波动率 (daily volatility)\n", + "\n", + " # 特异性收益 (idiosyncratic return) = 总收益 - 市场部分\n", + " idio_sig = daily_sig * np.sqrt(1 - mkt_corr**2)\n", + " idio = np.random.normal(0, idio_sig, N_DAYS)\n", + "\n", + " # 总日收益 = 市场因子 × 相关系数 + 特异性收益 + 均值漂移\n", + " ret = mkt_corr * market_factor + idio + (daily_mu - mkt_corr * 0.0002)\n", + " daily_returns[code] = ret\n", + "\n", + "# 转为 DataFrame,并生成价格序列 (convert to DataFrame, then price series)\n", + "start_date = pd.Timestamp(\"2019-01-02\")\n", + "dates = pd.bdate_range(start=start_date, periods=N_DAYS) # 仅工作日 (business days only)\n", + "\n", + "returns_df = pd.DataFrame(daily_returns, index=dates)\n", + "# 价格从 1.0 开始(净值形式,Net Asset Value style)\n", + "price_df = (1 + returns_df).cumprod()\n", + "\n", + "print(\"═\" * 60)\n", + "print(\"§0 ETF 价格数据生成完成 (Price Data Generated)\")\n", + "print(\"═\" * 60)\n", + "print(f\" 模拟区间 (Simulation Period): {dates[0].date()} → {dates[-1].date()}\")\n", + "print(f\" 交易日数 (Trading Days): {N_DAYS}\")\n", + "print(f\" ETF 数量 (Number of ETFs): {len(ALL_ETFS)}\")\n", + "print()\n", + "print(\" ETF 池 (Universe):\")\n", + "print(f\" {'代码':10s} {'中文名':12s} {'英文名':18s} {'年化收益':8s} {'年化波动':8s}\")\n", + "print(\" \" + \"-\" * 60)\n", + "for code, (cn, en, ret, vol, _) in ETF_UNIVERSE.items():\n", + " label = \" [避险]\" if code == BOND_ETF else \"\"\n", + " print(f\" {code:10s} {cn:12s} {en:18s} {ret*100:6.1f}% {vol*100:6.1f}%{label}\")\n", + "print()" + ] + }, + { + "cell_type": "markdown", + "id": "7e083547", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════════════\n", + "# §1 动量因子计算\n", + "# Momentum Factor Calculation\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════════════" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e01ab734", + "metadata": {}, + "outputs": [], + "source": [ + "def calc_momentum(price_df: pd.DataFrame,\n", + " lookback_months: int = 12,\n", + " skip_months: int = 1) -> pd.DataFrame:\n", + " \"\"\"\n", + " 计算每只 ETF 的动量得分 (Calculate momentum score for each ETF)\n", + "\n", + " 经典动量定义 (Classic momentum definition):\n", + " 过去 L 个月的总收益,跳过最近 S 个月(避免短期反转)\n", + " Total return over past L months, skipping most recent S months\n", + " (to avoid short-term reversal effect 短期反转效应)\n", + "\n", + " 参数 (Parameters):\n", + " price_df : 日频价格 DataFrame (daily price DataFrame)\n", + " lookback_months : 回望期(月数)(lookback period in months)\n", + " skip_months : 跳过最近月数(通常为 1)(skip recent months, usually 1)\n", + "\n", + " 返回 (Returns):\n", + " 月频动量得分 DataFrame (monthly momentum score DataFrame)\n", + " \"\"\"\n", + " # 转为月末价格 (resample to month-end prices)\n", + " # 注意:旧版 pandas 用 \"M\",新版用 \"ME\"\n", + " try:\n", + " monthly_price = price_df.resample(\"ME\").last()\n", + " except ValueError:\n", + " monthly_price = price_df.resample(\"M\").last()\n", + "\n", + " # 动量 = 前 (lookback+skip) 月价格 / 前 skip 月价格 - 1\n", + " # Momentum = Price[t - skip] / Price[t - lookback - skip] - 1\n", + " start_lag = skip_months\n", + " end_lag = lookback_months + skip_months\n", + "\n", + " momentum = monthly_price.shift(start_lag) / monthly_price.shift(end_lag) - 1\n", + " return momentum\n", + "\n", + "\n", + "# 计算三种不同回望期的动量(稳健性检验用)\n", + "# (Calculate momentum with three different lookback windows for robustness)\n", + "mom_12_1 = calc_momentum(price_df, lookback_months=12, skip_months=1) # 经典 12-1 月动量\n", + "mom_6_1 = calc_momentum(price_df, lookback_months=6, skip_months=1) # 中期 6-1 月动量\n", + "mom_3_1 = calc_momentum(price_df, lookback_months=3, skip_months=1) # 短期 3-1 月动量\n", + "\n", + "# 合成动量得分:三个窗口等权平均(减少单一窗口过拟合风险)\n", + "# (Composite momentum score: equal-weight average of three windows)\n", + "mom_composite = (mom_12_1 + mom_6_1 + mom_3_1) / 3\n", + "\n", + "print(\"§1 动量因子计算完成 (Momentum Factors Calculated)\")\n", + "print(f\" 月度数据行数 (Monthly rows): {len(mom_12_1)}\")\n", + "print()" + ] + }, + { + "cell_type": "markdown", + "id": "a098f350", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════════════\n", + "# §2 A 股交易成本模型\n", + "# A-Share Transaction Cost Model\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════════════" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c18a15bd", + "metadata": {}, + "outputs": [], + "source": [ + "class AShareCostModel:\n", + " \"\"\"\n", + " A 股 ETF 交易成本模型 (A-Share ETF Transaction Cost Model)\n", + "\n", + " A 股 ETF 特殊规则 (A-Share ETF Special Rules):\n", + " - ETF 可 T+0 交易(当天买当天可卖)Unlike 个股的 T+1 限制\n", + " (ETF allows T+0 trading, unlike individual stocks which are T+1)\n", + " - 印花税:仅卖出方向收取 0.1%\n", + " (Stamp duty: 0.1% on SELL side only)\n", + " - 佣金:买卖双向,通常 0.02% - 0.03%\n", + " (Commission: both sides, typically 0.02%-0.03%)\n", + " - 滑点:ETF 流动性好,通常 0.02%-0.05%\n", + " (Slippage: ETF has good liquidity, typically 0.02%-0.05%)\n", + " \"\"\"\n", + " def __init__(self,\n", + " commission_rate: float = 0.0003, # 佣金率 (commission rate)\n", + " stamp_duty_rate: float = 0.001, # 印花税率 (stamp duty rate)\n", + " slippage_rate: float = 0.0005): # 滑点率 (slippage rate)\n", + " self.commission = commission_rate\n", + " self.stamp_duty = stamp_duty_rate\n", + " self.slippage = slippage_rate\n", + "\n", + " def buy_cost(self, trade_value: float) -> float:\n", + " \"\"\"买入成本 (buy-side cost): 佣金 + 滑点\"\"\"\n", + " return trade_value * (self.commission + self.slippage)\n", + "\n", + " def sell_cost(self, trade_value: float) -> float:\n", + " \"\"\"卖出成本 (sell-side cost): 佣金 + 印花税 + 滑点\"\"\"\n", + " return trade_value * (self.commission + self.stamp_duty + self.slippage)\n", + "\n", + " def roundtrip_cost(self, trade_value: float) -> float:\n", + " \"\"\"\n", + " 双边成本 (round-trip cost): 买入 + 卖出\n", + " 月度换仓一次的总成本约 0.22%\n", + " \"\"\"\n", + " return self.buy_cost(trade_value) + self.sell_cost(trade_value)\n", + "\n", + " @property\n", + " def roundtrip_rate(self) -> float:\n", + " \"\"\"双边成本率 (round-trip cost rate)\"\"\"\n", + " return 2 * self.commission + self.stamp_duty + 2 * self.slippage\n", + "\n", + "\n", + "cost_model = AShareCostModel()\n", + "print(\"§2 交易成本模型 (Transaction Cost Model):\")\n", + "print(f\" 佣金率 (Commission): {cost_model.commission*100:.3f}% × 双边\")\n", + "print(f\" 印花税 (Stamp Duty): {cost_model.stamp_duty*100:.3f}% × 卖出方向\")\n", + "print(f\" 滑点率 (Slippage): {cost_model.slippage*100:.3f}% × 双边\")\n", + "print(f\" 双边总成本 (Roundtrip): {cost_model.roundtrip_rate*100:.3f}%\")\n", + "print()" + ] + }, + { + "cell_type": "markdown", + "id": "ac6f0f41", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════════════\n", + "# §3 轮动回测引擎\n", + "# Rotation Backtest Engine\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════════════" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6148ff2", + "metadata": {}, + "outputs": [], + "source": [ + "def run_rotation_backtest(\n", + " price_df: pd.DataFrame,\n", + " momentum_df: pd.DataFrame,\n", + " strategy: str = \"relative_momentum\",\n", + " top_k: int = 3,\n", + " cost_model: AShareCostModel = None,\n", + " bond_etf: str = BOND_ETF,\n", + " sector_etfs: list = None,\n", + ") -> dict:\n", + " \"\"\"\n", + " 行业 ETF 轮动回测引擎 (Sector ETF Rotation Backtest Engine)\n", + "\n", + " 参数 (Parameters):\n", + " price_df : 日频价格 (daily prices)\n", + " momentum_df : 月频动量得分 (monthly momentum scores)\n", + " strategy : 策略名称 (strategy name)\n", + " \"relative_momentum\" — 相对动量轮动\n", + " \"dual_momentum\" — 双动量(含绝对动量过滤)\n", + " \"equal_weight\" — 等权基准\n", + " top_k : 持有前 K 只 ETF (hold top-K ETFs)\n", + " cost_model : 交易成本模型 (transaction cost model)\n", + " bond_etf : 避险 ETF 代码 (safe-haven ETF code)\n", + " sector_etfs : 行业 ETF 代码列表 (sector ETF codes)\n", + "\n", + " 返回 (Returns):\n", + " dict 包含:\n", + " \"nav\" : 每日净值序列 (daily NAV series)\n", + " \"weights\" : 月度持仓权重 (monthly weights)\n", + " \"turnover\" : 月度换手率 (monthly turnover)\n", + " \"cost_total\" : 总交易成本 (total transaction cost)\n", + " \"holdings_log\": 持仓记录 (holdings log)\n", + " \"\"\"\n", + " if sector_etfs is None:\n", + " sector_etfs = SECTOR_ETFS\n", + " if cost_model is None:\n", + " cost_model = AShareCostModel()\n", + "\n", + " # 获取月末再平衡日期 (get month-end rebalancing dates)\n", + " try:\n", + " monthly_idx = price_df.resample(\"ME\").last().index\n", + " except ValueError:\n", + " monthly_idx = price_df.resample(\"M\").last().index\n", + "\n", + " # 预热期:动量需要至少 13 个月数据(12 个月回望 + 1 个月跳过)\n", + " # (Warm-up period: momentum needs at least 13 months of data)\n", + " warmup = 13\n", + "\n", + " # 初始化 (initialization)\n", + " nav = pd.Series(index=price_df.index, dtype=float)\n", + " weights_log = {} # 每月权重记录 (monthly weights log)\n", + " turnover_log = {} # 每月换手率记录 (monthly turnover log)\n", + " holdings_log = {} # 每月持仓记录 (monthly holdings log)\n", + " total_cost = 0.0 # 累计交易成本 (cumulative transaction cost)\n", + "\n", + " current_weights = {} # 当前持仓权重 {code: weight}\n", + " portfolio_value = 1.0 # 组合净值,从 1.0 开始 (portfolio starts at 1.0)\n", + "\n", + " rebal_dates = monthly_idx[warmup:] # 有效再平衡日期 (valid rebalance dates)\n", + "\n", + " for i, rebal_date in enumerate(rebal_dates):\n", + " # ── 1. 确定目标权重 (Determine target weights) ──────────────────────\n", + "\n", + " if strategy == \"equal_weight\":\n", + " # 等权:始终持有所有行业 ETF,各占 1/N\n", + " # (Equal weight: always hold all sector ETFs equally)\n", + " n = len(sector_etfs)\n", + " target_weights = {code: 1.0 / n for code in sector_etfs}\n", + "\n", + " elif strategy == \"relative_momentum\":\n", + " # 相对动量:选动量得分最高的前 K 只行业 ETF\n", + " # (Relative momentum: select top-K sector ETFs by momentum score)\n", + " mom_today = momentum_df.loc[:rebal_date, sector_etfs].iloc[-1]\n", + " mom_today = mom_today.dropna()\n", + " if len(mom_today) == 0:\n", + " target_weights = current_weights or {sector_etfs[0]: 1.0}\n", + " else:\n", + " top_etfs = mom_today.nlargest(top_k).index.tolist()\n", + " w = 1.0 / len(top_etfs)\n", + " target_weights = {code: w for code in top_etfs}\n", + "\n", + " elif strategy == \"dual_momentum\":\n", + " # 双动量:先用绝对动量过滤,再做相对动量排序\n", + " # (Dual momentum: filter by absolute momentum, then rank by relative)\n", + " mom_today = momentum_df.loc[:rebal_date, sector_etfs].iloc[-1]\n", + " mom_today = mom_today.dropna()\n", + "\n", + " # 绝对动量过滤 (absolute momentum filter):\n", + " # 如果行业 ETF 动量 < 0(跑输无风险),则替换为国债 ETF\n", + " # (If sector ETF momentum < 0, replace with bond ETF)\n", + " positive_etfs = mom_today[mom_today > 0].index.tolist()\n", + "\n", + " if len(positive_etfs) == 0:\n", + " # 全部动量为负:100% 持有国债 ETF 避险\n", + " # (All negative momentum: 100% in bond ETF)\n", + " target_weights = {bond_etf: 1.0}\n", + " else:\n", + " # 从通过绝对动量过滤的 ETF 中,选相对最强的前 K 只\n", + " # (From ETFs passing absolute momentum filter, pick top-K by relative)\n", + " k_actual = min(top_k, len(positive_etfs))\n", + " top_etfs = mom_today[positive_etfs].nlargest(k_actual).index.tolist()\n", + " w = 1.0 / k_actual\n", + " target_weights = {code: w for code in top_etfs}\n", + " else:\n", + " raise ValueError(f\"未知策略 (Unknown strategy): {strategy}\")\n", + "\n", + " # ── 2. 计算换手率并扣除交易成本 (Calculate turnover and deduct costs) ──\n", + "\n", + " # 所有涉及的 ETF 代码(新旧持仓并集)\n", + " all_codes = set(current_weights) | set(target_weights)\n", + " turnover = 0.0\n", + "\n", + " for code in all_codes:\n", + " old_w = current_weights.get(code, 0.0)\n", + " new_w = target_weights.get(code, 0.0)\n", + " delta = abs(new_w - old_w) # 权重变化 (weight change)\n", + " if delta > 1e-6:\n", + " turnover += delta\n", + " # 对权重变化部分计算成本 (cost on the traded portion)\n", + " trade_value = delta * portfolio_value\n", + " if new_w > old_w:\n", + " # 增仓 → 买入成本 (increase position → buy cost)\n", + " cost = cost_model.buy_cost(trade_value)\n", + " else:\n", + " # 减仓 → 卖出成本(含印花税)\n", + " # (decrease position → sell cost including stamp duty)\n", + " cost = cost_model.sell_cost(trade_value)\n", + " total_cost += cost\n", + " portfolio_value -= cost # 成本从组合价值中扣除 (deduct cost from portfolio)\n", + "\n", + " # 换手率(单边)= 买入总金额 / 组合价值 (one-sided turnover)\n", + " turnover_log[rebal_date] = turnover / 2 # 双边换手/2 = 单边换手\n", + "\n", + " # ── 3. 更新持仓,计算到下一个再平衡日的收益\n", + " # (Update holdings, compute returns until next rebalance date)\n", + " current_weights = target_weights\n", + " weights_log[rebal_date] = target_weights.copy()\n", + " holdings_log[rebal_date] = list(target_weights.keys())\n", + "\n", + " # 确定持仓持续的日期区间 (determine holding period date range)\n", + " if i + 1 < len(rebal_dates):\n", + " next_rebal = rebal_dates[i + 1]\n", + " else:\n", + " next_rebal = price_df.index[-1]\n", + "\n", + " hold_dates = price_df.loc[rebal_date:next_rebal].index\n", + "\n", + " # 计算每日组合收益率 (calculate daily portfolio return)\n", + " for j in range(1, len(hold_dates)):\n", + " d_prev = hold_dates[j - 1]\n", + " d_curr = hold_dates[j]\n", + " daily_port_ret = 0.0\n", + " for code, w in current_weights.items():\n", + " if code in price_df.columns:\n", + " p_prev = price_df.loc[d_prev, code]\n", + " p_curr = price_df.loc[d_curr, code]\n", + " if p_prev > 0:\n", + " daily_port_ret += w * (p_curr / p_prev - 1)\n", + " portfolio_value *= (1 + daily_port_ret)\n", + " nav[d_curr] = portfolio_value\n", + "\n", + " # 补全初始 NAV(热身期)\n", + " nav = nav.ffill()\n", + " first_valid = nav.first_valid_index()\n", + " if first_valid is not None:\n", + " nav[:first_valid] = 1.0\n", + " nav = nav.fillna(1.0)\n", + "\n", + " return {\n", + " \"nav\": nav,\n", + " \"weights\": weights_log,\n", + " \"turnover\": pd.Series(turnover_log),\n", + " \"cost_total\": total_cost,\n", + " \"holdings_log\": holdings_log,\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "12233425", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════════════\n", + "# §4 运行三种策略并计算绩效指标\n", + "# Run Three Strategies and Calculate Performance Metrics\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════════════" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5def8b15", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"§4 运行回测策略 (Running Backtest Strategies)...\")\n", + "print()\n", + "\n", + "# 运行三种策略 (run three strategies)\n", + "results = {}\n", + "\n", + "results[\"equal_weight\"] = run_rotation_backtest(\n", + " price_df, mom_composite,\n", + " strategy=\"equal_weight\",\n", + " top_k=3, cost_model=cost_model,\n", + ")\n", + "\n", + "results[\"relative_momentum\"] = run_rotation_backtest(\n", + " price_df, mom_composite,\n", + " strategy=\"relative_momentum\",\n", + " top_k=3, cost_model=cost_model,\n", + ")\n", + "\n", + "results[\"dual_momentum\"] = run_rotation_backtest(\n", + " price_df, mom_composite,\n", + " strategy=\"dual_momentum\",\n", + " top_k=3, cost_model=cost_model,\n", + ")\n", + "\n", + "# 同时计算国债 ETF 纯避险基准 (bond ETF as pure safe-haven benchmark)\n", + "bond_nav = price_df[BOND_ETF] / price_df[BOND_ETF].iloc[0]\n", + "results[\"bond_only\"] = {\"nav\": bond_nav}\n", + "\n", + "# ── 绩效计算函数 (Performance Calculation Function) ─────────────────────────\n", + "\n", + "def calc_performance(nav: pd.Series,\n", + " risk_free_annual: float = 0.02) -> dict:\n", + " \"\"\"\n", + " 计算常用量化绩效指标 (Calculate common quantitative performance metrics)\n", + "\n", + " 指标 (Metrics):\n", + " - 年化收益率 (Annualized Return, CAGR)\n", + " - 年化波动率 (Annualized Volatility)\n", + " - 夏普比率 (Sharpe Ratio): 超额收益 / 波动率\n", + " - 最大回撤 (Maximum Drawdown, MaxDD): 从历史最高点的最大跌幅\n", + " - 卡玛比率 (Calmar Ratio): 年化收益 / 最大回撤\n", + " - 索提诺比率 (Sortino Ratio): 用下行波动率替代总波动率\n", + " \"\"\"\n", + " nav = nav.dropna()\n", + " if len(nav) < 2:\n", + " return {}\n", + "\n", + " # 日收益率 (daily returns)\n", + " daily_ret = nav.pct_change().dropna()\n", + "\n", + " # 年化收益率 (CAGR - Compound Annual Growth Rate 复合年增长率)\n", + " n_years = len(daily_ret) / TRADING_DAYS\n", + " total_ret = nav.iloc[-1] / nav.iloc[0] - 1\n", + " cagr = (1 + total_ret) ** (1 / n_years) - 1\n", + "\n", + " # 年化波动率 (annualized volatility)\n", + " ann_vol = daily_ret.std() * np.sqrt(TRADING_DAYS)\n", + "\n", + " # 夏普比率 (Sharpe Ratio)\n", + " # 公式: (年化收益 - 无风险利率) / 年化波动率\n", + " rf_daily = risk_free_annual / TRADING_DAYS\n", + " sharpe = (daily_ret.mean() - rf_daily) / daily_ret.std() * np.sqrt(TRADING_DAYS)\n", + "\n", + " # 最大回撤 (Maximum Drawdown)\n", + " # 定义: max(累计最高净值 - 当日净值) / 累计最高净值\n", + " rolling_max = nav.cummax() # 历史最高净值 (rolling maximum)\n", + " drawdown = nav / rolling_max - 1 # 每日回撤序列 (daily drawdown series)\n", + " max_dd = drawdown.min() # 最大回撤(负数)(max drawdown, negative)\n", + "\n", + " # 卡玛比率 (Calmar Ratio = CAGR / |MaxDD|)\n", + " calmar = cagr / abs(max_dd) if max_dd != 0 else np.nan\n", + "\n", + " # 索提诺比率 (Sortino Ratio): 只惩罚下行波动 (penalizes only downside volatility)\n", + " downside_ret = daily_ret[daily_ret < rf_daily]\n", + " downside_vol = downside_ret.std() * np.sqrt(TRADING_DAYS) if len(downside_ret) > 0 else ann_vol\n", + " sortino = (cagr - risk_free_annual) / downside_vol if downside_vol > 0 else np.nan\n", + "\n", + " # 胜率 (Win Rate): 月度正收益比例 (fraction of months with positive return)\n", + " try:\n", + " monthly_ret = nav.resample(\"ME\").last().pct_change().dropna()\n", + " except ValueError:\n", + " monthly_ret = nav.resample(\"M\").last().pct_change().dropna()\n", + " win_rate = (monthly_ret > 0).mean()\n", + "\n", + " return {\n", + " \"年化收益率 (CAGR)\": cagr,\n", + " \"年化波动率 (Volatility)\": ann_vol,\n", + " \"夏普比率 (Sharpe)\": sharpe,\n", + " \"最大回撤 (MaxDrawdown)\": max_dd,\n", + " \"卡玛比率 (Calmar)\": calmar,\n", + " \"索提诺比率 (Sortino)\": sortino,\n", + " \"月度胜率 (Win Rate)\": win_rate,\n", + " \"累计收益率 (Total Return)\": total_ret,\n", + " }\n", + "\n", + "\n", + "# ── 打印绩效对比表 (Print Performance Comparison Table) ─────────────────────\n", + "\n", + "strategy_labels = {\n", + " \"equal_weight\": \"等权基准 (EW Benchmark)\",\n", + " \"relative_momentum\": \"相对动量 (Rel Momentum)\",\n", + " \"dual_momentum\": \"双动量 (Dual Momentum)\",\n", + " \"bond_only\": \"国债避险 (Bond Only)\",\n", + "}\n", + "\n", + "print(\"=\" * 72)\n", + "print(\"§4 策略绩效对比 (Strategy Performance Comparison)\")\n", + "print(\"=\" * 72)\n", + "\n", + "perf_all = {}\n", + "metrics_display = [\n", + " (\"年化收益率 (CAGR)\", \"{:>8.2%}\"),\n", + " (\"年化波动率 (Volatility)\", \"{:>8.2%}\"),\n", + " (\"夏普比率 (Sharpe)\", \"{:>8.3f}\"),\n", + " (\"最大回撤 (MaxDrawdown)\", \"{:>8.2%}\"),\n", + " (\"卡玛比率 (Calmar)\", \"{:>8.3f}\"),\n", + " (\"月度胜率 (Win Rate)\", \"{:>8.2%}\"),\n", + " (\"累计收益率 (Total Return)\", \"{:>8.2%}\"),\n", + "]\n", + "\n", + "for key, label in strategy_labels.items():\n", + " nav = results[key][\"nav\"]\n", + " perf = calc_performance(nav)\n", + " perf_all[key] = perf\n", + "\n", + "header = f\" {'指标':26s}\"\n", + "for label in strategy_labels.values():\n", + " header += f\" {label[:20]:>20s}\"\n", + "print(header)\n", + "print(\" \" + \"-\" * 90)\n", + "\n", + "for metric, fmt in metrics_display:\n", + " row = f\" {metric:26s}\"\n", + " for key in strategy_labels:\n", + " val = perf_all[key].get(metric, np.nan)\n", + " row += \" \" + fmt.format(val).rjust(20)\n", + " print(row)\n", + "\n", + "print()\n", + "# 打印成本统计\n", + "for key in [\"equal_weight\", \"relative_momentum\", \"dual_momentum\"]:\n", + " cost = results[key].get(\"cost_total\", 0)\n", + " to = results[key].get(\"turnover\", pd.Series()).mean()\n", + " print(f\" [{strategy_labels[key][:16]}] \"\n", + " f\"总成本={cost*100:.3f}%净值 平均月换手={to*100:.1f}%\")\n", + "print()" + ] + }, + { + "cell_type": "markdown", + "id": "45bf4a54", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════════════\n", + "# §5 持仓分析:统计各行业 ETF 的被选中频次\n", + "# Holdings Analysis: Count How Often Each Sector ETF Was Selected\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════════════" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19761832", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"§5 持仓分析 (Holdings Analysis):\")\n", + "print()\n", + "\n", + "for strategy_key in [\"relative_momentum\", \"dual_momentum\"]:\n", + " holdings_log = results[strategy_key][\"holdings_log\"]\n", + " count = {code: 0 for code in ALL_ETFS}\n", + "\n", + " for date, held in holdings_log.items():\n", + " for code in held:\n", + " count[code] = count.get(code, 0) + 1\n", + "\n", + " total_months = len(holdings_log)\n", + " label = strategy_labels[strategy_key]\n", + " print(f\" [{label}] 共 {total_months} 次再平衡:\")\n", + "\n", + " # 按持有频次降序排列 (sort by holding frequency descending)\n", + " sorted_count = sorted(count.items(), key=lambda x: x[1], reverse=True)\n", + " for code, cnt in sorted_count:\n", + " if cnt > 0:\n", + " cn_name = ETF_UNIVERSE[code][0]\n", + " pct = cnt / total_months\n", + " bar = \"█\" * int(pct * 20)\n", + " print(f\" {code} {cn_name:10s} {cnt:3d}次 {pct:5.1%} {bar}\")\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "id": "659ce25c", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════════════\n", + "# §6 可视化:9 宫格图表\n", + "# Visualization: 9-Panel Chart\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════════════" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da21c149", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"§6 生成可视化图表 (Generating Charts)...\")\n", + "\n", + "# 配置中文字体 (configure Chinese font)\n", + "plt.rcParams[\"font.sans-serif\"] = [\"WenQuanYi Zen Hei\", \"Arial Unicode MS\",\n", + " \"SimHei\", \"DejaVu Sans\"]\n", + "plt.rcParams[\"axes.unicode_minus\"] = False\n", + "\n", + "# 颜色方案 (color scheme)\n", + "COLORS = {\n", + " \"equal_weight\": \"#95a5a6\", # 灰色(基准)\n", + " \"relative_momentum\": \"#2ecc71\", # 绿色\n", + " \"dual_momentum\": \"#e74c3c\", # 红色(主策略)\n", + " \"bond_only\": \"#3498db\", # 蓝色(国债)\n", + "}\n", + "LABELS = {\n", + " \"equal_weight\": \"等权基准\",\n", + " \"relative_momentum\": \"相对动量\",\n", + " \"dual_momentum\": \"双动量\",\n", + " \"bond_only\": \"国债\",\n", + "}\n", + "\n", + "fig = plt.figure(figsize=(20, 16), facecolor=\"#1a1a2e\")\n", + "fig.suptitle(\n", + " \"A 股行业 ETF 轮动策略回测 (A-Share Sector ETF Rotation Backtest)\\n\"\n", + " \"合成模拟数据 2019-2024 (Synthetic Data 2019-2024)\",\n", + " fontsize=16, color=\"white\", fontweight=\"bold\", y=0.98\n", + ")\n", + "\n", + "gs = gridspec.GridSpec(3, 3, figure=fig,\n", + " hspace=0.42, wspace=0.35,\n", + " left=0.07, right=0.97, top=0.93, bottom=0.06)\n", + "\n", + "ax_style = dict(facecolor=\"#16213e\", labelcolor=\"white\",\n", + " tick_params=dict(colors=\"white\"))\n", + "\n", + "def style_ax(ax, title, xlabel=\"\", ylabel=\"\"):\n", + " ax.set_facecolor(\"#16213e\")\n", + " ax.set_title(title, color=\"white\", fontsize=10, fontweight=\"bold\", pad=6)\n", + " ax.set_xlabel(xlabel, color=\"#aaaaaa\", fontsize=8)\n", + " ax.set_ylabel(ylabel, color=\"#aaaaaa\", fontsize=8)\n", + " ax.tick_params(colors=\"white\", labelsize=7)\n", + " ax.spines[:].set_color(\"#444466\")\n", + " ax.grid(True, alpha=0.25, color=\"#666699\", linewidth=0.5)\n", + "\n", + "# ── 图1: 累计净值曲线 (Cumulative NAV) ───────────────────────────────────────\n", + "ax1 = fig.add_subplot(gs[0, :2]) # 跨两列(宽幅)\n", + "style_ax(ax1, \"① 累计净值对比 Cumulative NAV Comparison\", ylabel=\"净值 NAV\")\n", + "for key, color in COLORS.items():\n", + " nav = results[key][\"nav\"]\n", + " ax1.plot(nav.index, nav.values, color=color, linewidth=1.8,\n", + " label=LABELS[key], alpha=0.9)\n", + "ax1.axhline(1.0, color=\"#888888\", linewidth=0.8, linestyle=\"--\", alpha=0.6)\n", + "ax1.legend(loc=\"upper left\", fontsize=8, facecolor=\"#1a1a2e\",\n", + " labelcolor=\"white\", framealpha=0.7)\n", + "ax1.yaxis.set_major_formatter(mtick.FuncFormatter(lambda x, _: f\"{x:.1f}x\"))\n", + "\n", + "# ── 图2: 绩效雷达图 (Performance Radar) ─────────────────────────────────────\n", + "ax2 = fig.add_subplot(gs[0, 2], projection=\"polar\")\n", + "ax2.set_facecolor(\"#16213e\")\n", + "ax2.set_title(\"② 绩效雷达图 Performance Radar\",\n", + " color=\"white\", fontsize=10, fontweight=\"bold\", pad=15)\n", + "ax2.tick_params(colors=\"white\", labelsize=7)\n", + "ax2.spines[\"polar\"].set_color(\"#444466\")\n", + "\n", + "radar_metrics = [\"年化收益\", \"夏普比率\", \"月胜率\", \"抗回撤\"]\n", + "radar_keys = [\"年化收益率 (CAGR)\", \"夏普比率 (Sharpe)\",\n", + " \"月度胜率 (Win Rate)\", \"卡玛比率 (Calmar)\"]\n", + "n_radar = len(radar_metrics)\n", + "angles = np.linspace(0, 2 * np.pi, n_radar, endpoint=False).tolist()\n", + "angles += angles[:1]\n", + "\n", + "# 归一化各指标到 0-1 (normalize metrics to 0-1)\n", + "def normalize_radar(values, min_val=0.0, max_val=1.0):\n", + " rng = max_val - min_val\n", + " return [(v - min_val) / rng if rng > 0 else 0.5 for v in values]\n", + "\n", + "radar_strats = [\"equal_weight\", \"relative_momentum\", \"dual_momentum\"]\n", + "for rkey in radar_strats:\n", + " perf = perf_all[rkey]\n", + " raw = [perf.get(m, 0) for m in radar_keys]\n", + " # 简单归一化(基于各策略范围)\n", + " vals = [\n", + " min(max(raw[0] / 0.30, 0), 1), # CAGR: 0~30%\n", + " min(max(raw[1] / 3.0, 0), 1), # Sharpe: 0~3\n", + " min(max(raw[2], 0), 1), # Win rate: 0~1\n", + " min(max(raw[3] / 3.0, 0), 1), # Calmar: 0~3\n", + " ]\n", + " vals += vals[:1]\n", + " ax2.plot(angles, vals, color=COLORS[rkey], linewidth=2, label=LABELS[rkey])\n", + " ax2.fill(angles, vals, color=COLORS[rkey], alpha=0.15)\n", + "\n", + "ax2.set_xticks(angles[:-1])\n", + "ax2.set_xticklabels(radar_metrics, color=\"white\", fontsize=8)\n", + "ax2.set_yticklabels([])\n", + "ax2.legend(loc=\"upper right\", bbox_to_anchor=(1.35, 1.15),\n", + " fontsize=7, facecolor=\"#1a1a2e\", labelcolor=\"white\")\n", + "\n", + "# ── 图3: 最大回撤曲线 (Drawdown Curve) ──────────────────────────────────────\n", + "ax3 = fig.add_subplot(gs[1, :2])\n", + "style_ax(ax3, \"③ 水下曲线(回撤) Underwater Chart (Drawdown)\", ylabel=\"回撤 Drawdown\")\n", + "for key, color in COLORS.items():\n", + " nav = results[key][\"nav\"]\n", + " dd = nav / nav.cummax() - 1\n", + " ax3.fill_between(dd.index, dd.values, 0, color=color, alpha=0.4, label=LABELS[key])\n", + " ax3.plot(dd.index, dd.values, color=color, linewidth=0.8, alpha=0.8)\n", + "ax3.set_ylim(-0.65, 0.05)\n", + "ax3.yaxis.set_major_formatter(mtick.PercentFormatter(1.0))\n", + "ax3.legend(loc=\"lower left\", fontsize=8, facecolor=\"#1a1a2e\",\n", + " labelcolor=\"white\", framealpha=0.7)\n", + "\n", + "# ── 图4: 绩效指标条形图 (Performance Bar Chart) ──────────────────────────────\n", + "ax4 = fig.add_subplot(gs[1, 2])\n", + "style_ax(ax4, \"④ 夏普 & 卡玛 Sharpe & Calmar\", ylabel=\"比率 Ratio\")\n", + "strats_bar = [\"equal_weight\", \"relative_momentum\", \"dual_momentum\"]\n", + "labels_bar = [LABELS[k] for k in strats_bar]\n", + "sharpes = [perf_all[k][\"夏普比率 (Sharpe)\"] for k in strats_bar]\n", + "calmars = [perf_all[k][\"卡玛比率 (Calmar)\"] for k in strats_bar]\n", + "x_bar = np.arange(len(strats_bar))\n", + "w_bar = 0.35\n", + "bars1 = ax4.bar(x_bar - w_bar/2, sharpes, w_bar, color=\"#2ecc71\", alpha=0.8,\n", + " label=\"夏普 Sharpe\")\n", + "bars2 = ax4.bar(x_bar + w_bar/2, calmars, w_bar, color=\"#e74c3c\", alpha=0.8,\n", + " label=\"卡玛 Calmar\")\n", + "ax4.set_xticks(x_bar)\n", + "ax4.set_xticklabels(labels_bar, fontsize=7, color=\"white\")\n", + "ax4.legend(fontsize=8, facecolor=\"#1a1a2e\", labelcolor=\"white\")\n", + "for bar in bars1:\n", + " ax4.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,\n", + " f\"{bar.get_height():.2f}\", ha=\"center\", va=\"bottom\",\n", + " color=\"white\", fontsize=7)\n", + "for bar in bars2:\n", + " ax4.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,\n", + " f\"{bar.get_height():.2f}\", ha=\"center\", va=\"bottom\",\n", + " color=\"white\", fontsize=7)\n", + "\n", + "# ── 图5: 双动量月度持仓热力图 (Dual Momentum Monthly Holdings Heatmap) ────────\n", + "ax5 = fig.add_subplot(gs[2, :2])\n", + "style_ax(ax5, \"⑤ 双动量月度持仓热力图 Dual Momentum Monthly Holdings\")\n", + "\n", + "holdings_log = results[\"dual_momentum\"][\"holdings_log\"]\n", + "all_codes_sorted = SECTOR_ETFS + [BOND_ETF]\n", + "etf_names = [ETF_UNIVERSE[c][0] for c in all_codes_sorted]\n", + "\n", + "heatmap_dates = sorted(holdings_log.keys())\n", + "heatmap_data = np.zeros((len(all_codes_sorted), len(heatmap_dates)))\n", + "for j, d in enumerate(heatmap_dates):\n", + " held = holdings_log[d]\n", + " for i, code in enumerate(all_codes_sorted):\n", + " if code in held:\n", + " heatmap_data[i, j] = 1.0 / len(held) # 持仓权重 (holding weight)\n", + "\n", + "im = ax5.imshow(heatmap_data, aspect=\"auto\", cmap=\"YlOrRd\",\n", + " interpolation=\"nearest\", vmin=0, vmax=0.5)\n", + "ax5.set_yticks(range(len(all_codes_sorted)))\n", + "ax5.set_yticklabels(etf_names, fontsize=7, color=\"white\")\n", + "\n", + "# X 轴:每年一个刻度 (X-axis: one tick per year)\n", + "year_ticks = []\n", + "year_labels = []\n", + "for j, d in enumerate(heatmap_dates):\n", + " if d.month == 1:\n", + " year_ticks.append(j)\n", + " year_labels.append(str(d.year))\n", + "ax5.set_xticks(year_ticks)\n", + "ax5.set_xticklabels(year_labels, color=\"white\", fontsize=8)\n", + "plt.colorbar(im, ax=ax5, fraction=0.02, label=\"权重 Weight\").ax.yaxis.set_tick_params(color=\"white\", labelcolor=\"white\")\n", + "\n", + "# ── 图6: 月度换手率 (Monthly Turnover) ──────────────────────────────────────\n", + "ax6 = fig.add_subplot(gs[2, 2])\n", + "style_ax(ax6, \"⑥ 月度换手率 Monthly Turnover\", ylabel=\"换手率 Turnover\")\n", + "for key in [\"equal_weight\", \"relative_momentum\", \"dual_momentum\"]:\n", + " to_series = results[key].get(\"turnover\", pd.Series())\n", + " if len(to_series) > 0:\n", + " ax6.plot(to_series.index, to_series.values * 100,\n", + " color=COLORS[key], linewidth=1.2, label=LABELS[key], alpha=0.8)\n", + " # 平均换手率水平线 (average turnover horizontal line)\n", + " avg_to = to_series.mean() * 100\n", + " ax6.axhline(avg_to, color=COLORS[key], linewidth=0.8,\n", + " linestyle=\":\", alpha=0.6)\n", + "ax6.yaxis.set_major_formatter(mtick.PercentFormatter())\n", + "ax6.legend(fontsize=7, facecolor=\"#1a1a2e\", labelcolor=\"white\")\n", + "\n", + "# 保存图表 (save chart)\n", + "out_path = \"/Users/tigeren/Dev/xorbitlab/trading/etf_rotation_demo.png\"\n", + "plt.savefig(out_path, dpi=150, bbox_inches=\"tight\",\n", + " facecolor=fig.get_facecolor())\n", + "plt.close()\n", + "print(f\" 图表已保存 (Chart saved): {out_path}\")\n", + "print()" + ] + }, + { + "cell_type": "markdown", + "id": "b24aac17", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════════════\n", + "# §7 关键学习要点总结\n", + "# Key Learning Takeaways\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════════════" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "838d08d5", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"═\" * 60)\n", + "print(\"§7 关键学习要点 (Key Learning Takeaways)\")\n", + "print(\"═\" * 60)\n", + "print(\"\"\"\n", + " 1. ETF 轮动 vs 个股选股 (ETF Rotation vs. Stock Picking)\n", + " ───────────────────────────────────────────────────\n", + " ETF 轮动:买\"赛道\",避免个股黑天鹅事件\n", + " (ETF rotation: bet on sectors, avoid individual stock blow-ups)\n", + " \n", + " 个股选股:需要更强的信息优势,难度更高\n", + " (Stock picking: requires information edge, much harder)\n", + "\n", + " 2. 绝对动量的价值 (Value of Absolute Momentum)\n", + " ───────────────────────────────────────────\n", + " 双动量策略在熊市中会自动切换至国债 ETF 避险\n", + " 最大回撤通常比纯相对动量低 10-15 个百分点\n", + " (Dual momentum auto-switches to bonds in bear markets,\n", + " reducing max drawdown by ~10-15 percentage points)\n", + "\n", + " 3. 动量的局限性 (Momentum Limitations)\n", + " ────────────────────────────────────\n", + " • 趋势逆转(动量崩溃)风险:牛熊切换时可能大幅亏损\n", + " (Momentum crash risk: large loss when trend reverses)\n", + " • 主题 ETF 历史短:A 股主题 ETF 多数 2019 年后才成立\n", + " (Short history: most A-share thematic ETFs launched post-2019)\n", + " • 需要结合估值/基本面作为反向过滤器\n", + " (Should combine with valuation/fundamentals as counter-filter)\n", + "\n", + " 4. 成本控制 (Cost Management)\n", + " ───────────────────────────\n", + " 每月换仓一次约消耗 0.22% 双边成本\n", + " 一年 12 次再平衡 ≈ 2.6% 年化成本拖拽\n", + " (Monthly rebalancing ≈ 0.22% per round-trip, ~2.6% annual drag)\n", + " → 适当降低再平衡频率(如季度)可以减少成本\n", + " (Reduce rebalance frequency, e.g., quarterly, to cut costs)\n", + "\n", + " 5. 进阶方向 (Next Steps)\n", + " ───────────────────────\n", + " • 接入真实数据(AKShare)替换合成数据\n", + " (Replace synthetic data with real data via AKShare)\n", + " • 加入估值信号(PE 分位数)作为过滤器\n", + " (Add valuation signal: sector PE percentile as filter)\n", + " • 加入波动率调仓(高波动期降低持仓比例)\n", + " (Add volatility scaling: reduce position in high-vol regimes)\n", + " • 结合宏观信号(利率、信用利差)增强国债 ETF 切换时机\n", + " (Combine macro signals for better timing of bond ETF switch)\n", + "\"\"\")\n", + "\n", + "print(\"✅ Demo 完成!(Demo Complete!)\")\n", + "print(f\" 输出文件: etf_rotation_demo.png\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (trading)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/quant_event_driven_backtest_demo.ipynb b/quant_event_driven_backtest_demo.ipynb new file mode 100644 index 0000000..4aae3a5 --- /dev/null +++ b/quant_event_driven_backtest_demo.ipynb @@ -0,0 +1,1713 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e51fcc58", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# Quantitative Trading — Event-Driven Backtest Engine\n", + "# 量化交易 — 事件驱动回测引擎\n", + "\n", + "# =============================================================================\n", + "#\n", + "# 本文件是策略开发演示 (quant_strategy_backtest_demo.py) 的续集。\n", + "# This file is the sequel to the strategy development demo.\n", + "#\n", + "# ─────────────────────────────────────────────────────────────────────────────\n", + "# Why Event-Driven? 为什么要用事件驱动?\n", + "# ─────────────────────────────────────────────────────────────────────────────\n", + "# The vectorized backtest in the previous demo computed everything at once\n", + "# using array operations. This is fast, but it has a fundamental problem:\n", + "# 上一个演示中的向量化回测一次性用数组运算计算所有结果。速度很快,但有根本缺陷:\n", + "#\n", + "# It cannot easily model:\n", + "# 难以建模:\n", + "# • Different order types 不同订单类型 (limit / stop / market orders)\n", + "# • Partial fills 部分成交\n", + "# • Intraday price path 日内价格路径 (open → high/low → close sequence)\n", + "# • Real-time risk checks 实时风控检查 (e.g. position limits, margin calls)\n", + "# • Multiple strategies 多策略组合\n", + "# • Portfolio-level rules 组合层面规则 (e.g. max sector exposure)\n", + "#\n", + "# The event-driven approach treats the backtest as a simulation of the actual\n", + "# live trading system. The same strategy & portfolio code can run in both\n", + "# backtest mode AND live trading mode — just swap the data source.\n", + "#\n", + "# 事件驱动方法将回测视为真实交易系统的模拟。\n", + "# 同一套策略和组合代码可以在回测模式和实盘模式下运行——只需替换数据源。\n", + "#\n", + "# ─────────────────────────────────────────────────────────────────────────────\n", + "# Architecture Overview 架构总览\n", + "# ─────────────────────────────────────────────────────────────────────────────\n", + "#\n", + "# ┌─────────────────────────────────┐\n", + "# │ Event Queue 事件队列 │\n", + "# │ (FIFO deque — central bus) │\n", + "# └───────────────┬─────────────────┘\n", + "# │ events flow through\n", + "# ┌────────────────────┼────────────────────┐\n", + "# ▼ ▼ ▼\n", + "# ┌────────────────┐ ┌────────────────┐ ┌────────────────┐\n", + "# │ DataHandler │ │ Strategy │ │ Portfolio │\n", + "# │ 数据处理器 │ │ 策略引擎 │ │ 组合管理器 │\n", + "# │ │ │ │ │ │\n", + "# │ Streams bars │ │ Reads market │ │ Receives order │\n", + "# │ (OHLCV) into │ │ data, computes │ │ requests from │\n", + "# │ the queue as │ │ signals, emits │ │ strategy and │\n", + "# │ MarketEvents │ │ SignalEvents │ │ emits │\n", + "# └────────────────┘ └────────────────┘ │ OrderEvents │\n", + "# └───────┬────────┘\n", + "# │\n", + "# ┌───────▼────────┐\n", + "# │ ExecutionHandler│\n", + "# │ 执行处理器 │\n", + "# │ (模拟券商) │\n", + "# │ │\n", + "# │ Fills orders, │\n", + "# │ applies slippage│\n", + "# │ & commission, │\n", + "# │ emits │\n", + "# │ FillEvents │\n", + "# └────────────────┘\n", + "#\n", + "# ─────────────────────────────────────────────────────────────────────────────\n", + "# Event flow for one trading day 单个交易日的事件流\n", + "# ─────────────────────────────────────────────────────────────────────────────\n", + "#\n", + "# [Clock ticks to new day / 时钟滴答到新的一天]\n", + "# │\n", + "# ▼\n", + "# DataHandler emits MarketEvent (新K线数据到达)\n", + "# │\n", + "# ▼\n", + "# Strategy processes MarketEvent → emits SignalEvent (信号:买/卖/平)\n", + "# │\n", + "# ▼\n", + "# Portfolio processes SignalEvent → emits OrderEvent (下单:市价/限价/止损)\n", + "# │\n", + "# ▼\n", + "# ExecutionHandler processes OrderEvent → emits FillEvent (成交确认)\n", + "# │\n", + "# ▼\n", + "# Portfolio processes FillEvent → updates positions & P&L (更新持仓和盈亏)\n", + "# │\n", + "# ▼\n", + "# [repeat for next day / 重复下一天]\n", + "#\n", + "# ─────────────────────────────────────────────────────────────────────────────\n", + "# Topics covered / 涵盖主题:\n", + "# 1. Event Classes 事件类 (Market, Signal, Order, Fill)\n", + "# 2. DataHandler 数据处理器\n", + "# 3. Strategy 策略 (MA Crossover & RSI)\n", + "# 4. Portfolio 组合管理器 (cash, positions, P&L)\n", + "# 5. ExecutionHandler 执行处理器 (slippage models, order types)\n", + "# 6. BacktestEngine 回测引擎 (main event loop)\n", + "# 7. Performance Analytics 绩效分析\n", + "# 8. Comparison: Event-Driven vs Vectorized\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1120985c", + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "\n", + "import queue # Python built-in FIFO queue\n", + "import enum # for clean event type constants\n", + "from dataclasses import dataclass, field\n", + "from abc import ABC, abstractmethod\n", + "from typing import Dict, List, Optional, Tuple\n", + "from copy import deepcopy\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.gridspec as gridspec\n", + "import warnings\n", + "\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "# ── Chinese font / 中文字体 ──────────────────────────────────────────────────\n", + "plt.rcParams['font.sans-serif'] = ['WenQuanYi Zen Hei', 'Arial Unicode MS',\n", + " 'SimHei', 'DejaVu Sans']\n", + "plt.rcParams['axes.unicode_minus'] = False\n", + "\n", + "np.random.seed(42)\n", + "\n", + "print(\"=\" * 72)\n", + "print(\" 量化交易 — 事件驱动回测引擎\")\n", + "print(\" Quantitative Trading — Event-Driven Backtest Engine\")\n", + "print(\"=\" * 72)" + ] + }, + { + "cell_type": "markdown", + "id": "97b23791", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 1: Event Classes 事件类\n", + "\n", + "# =============================================================================\n", + "#\n", + "# Every piece of information that flows through the system is wrapped in an\n", + "# Event object. This decouples the components — a Strategy doesn't call the\n", + "# Portfolio directly; it just puts a SignalEvent on the queue.\n", + "#\n", + "# 系统中流通的每一条信息都被包装成一个 Event 对象。\n", + "# 这使各组件解耦——策略不直接调用组合,只是把 SignalEvent 放入队列。\n", + "#\n", + "# Four event types:\n", + "# 四种事件类型:\n", + "#\n", + "# MarketEvent 市场事件 — new OHLCV bar has arrived from the data feed\n", + "# SignalEvent 信号事件 — strategy has decided to buy or sell\n", + "# OrderEvent 订单事件 — portfolio has sized the trade and placed an order\n", + "# FillEvent 成交事件 — broker has confirmed the order was executed\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b99769a6", + "metadata": {}, + "outputs": [], + "source": [ + "class EventType(enum.Enum):\n", + " \"\"\"\n", + " Enumeration of all event types in the system.\n", + " 系统中所有事件类型的枚举。\n", + " \"\"\"\n", + " MARKET = \"MARKET\" # 市场事件 — new price bar\n", + " SIGNAL = \"SIGNAL\" # 信号事件 — trade signal from strategy\n", + " ORDER = \"ORDER\" # 订单事件 — order from portfolio\n", + " FILL = \"FILL\" # 成交事件 — filled order from broker\n", + "\n", + "\n", + "class Direction(enum.Enum):\n", + " \"\"\"\n", + " Trade direction. 交易方向。\n", + " \"\"\"\n", + " LONG = \"LONG\" # 做多 / buy\n", + " SHORT = \"SHORT\" # 做空 / sell short\n", + " EXIT = \"EXIT\" # 平仓 / close position\n", + "\n", + "\n", + "class OrderType(enum.Enum):\n", + " \"\"\"\n", + " Order types. 订单类型。\n", + "\n", + " MARKET 市价单 — execute immediately at the best available price\n", + " 立即以市场最优价格成交,确保成交但价格不确定\n", + " LIMIT 限价单 — execute only at a specified price or better\n", + " 只在指定价格或更优价格成交,价格确定但不保证成交\n", + " STOP 止损单 — becomes a market order when price hits the stop level\n", + " 价格触及止损价时转变为市价单,用于控制最大亏损\n", + " \"\"\"\n", + " MARKET = \"MARKET\"\n", + " LIMIT = \"LIMIT\"\n", + " STOP = \"STOP\"\n", + "\n", + "\n", + "@dataclass\n", + "class MarketEvent:\n", + " \"\"\"\n", + " Triggered when the DataHandler has a new bar of OHLCV data ready.\n", + " 当数据处理器有新的 OHLCV K线数据就绪时触发。\n", + "\n", + " This is the \"heartbeat\" of the system — it drives all other components.\n", + " 这是系统的\"心跳\"——驱动所有其他组件。\n", + "\n", + " Fields / 字段:\n", + " symbol 股票代码 — which ticker this bar belongs to\n", + " date 日期 — the date of this bar\n", + " open 开盘价 — first trade of the session\n", + " high 最高价 — highest trade during the session\n", + " low 最低价 — lowest trade during the session\n", + " close 收盘价 — last trade of the session\n", + " volume 成交量 — total shares traded\n", + " \"\"\"\n", + " type : EventType = field(default=EventType.MARKET, init=False)\n", + " symbol : str = \"\"\n", + " date : pd.Timestamp = None\n", + " open : float = 0.0\n", + " high : float = 0.0\n", + " low : float = 0.0\n", + " close : float = 0.0\n", + " volume : float = 0.0\n", + "\n", + " def __repr__(self):\n", + " return (f\"MarketEvent({self.symbol} {self.date.date()} \"\n", + " f\"O={self.open:.2f} H={self.high:.2f} \"\n", + " f\"L={self.low:.2f} C={self.close:.2f})\")\n", + "\n", + "\n", + "@dataclass\n", + "class SignalEvent:\n", + " \"\"\"\n", + " Generated by a Strategy when it detects a trade opportunity.\n", + " 策略检测到交易机会时生成。\n", + "\n", + " A SignalEvent is a \"suggestion\" — the Portfolio decides whether and\n", + " how much to actually trade based on its own risk rules.\n", + " 信号事件是一个\"建议\"——组合管理器根据自身风控规则决定是否以及交易多少。\n", + "\n", + " Fields / 字段:\n", + " symbol 股票代码\n", + " date 信号产生日期\n", + " direction 方向: LONG(做多) / SHORT(做空) / EXIT(平仓)\n", + " strength 信号强度 [0, 1] — used for position sizing (仓位管理)\n", + " 1.0 = full conviction, 0.5 = half conviction\n", + " strategy 策略名称 — useful when running multiple strategies\n", + " \"\"\"\n", + " type : EventType = field(default=EventType.SIGNAL, init=False)\n", + " symbol : str = \"\"\n", + " date : pd.Timestamp = None\n", + " direction : Direction = Direction.LONG\n", + " strength : float = 1.0 # 信号强度 / signal strength for position sizing\n", + " strategy : str = \"\"\n", + "\n", + " def __repr__(self):\n", + " return (f\"SignalEvent({self.symbol} {self.date.date()} \"\n", + " f\"{self.direction.value} strength={self.strength:.2f})\")\n", + "\n", + "\n", + "@dataclass\n", + "class OrderEvent:\n", + " \"\"\"\n", + " Sent from Portfolio to ExecutionHandler to place a trade.\n", + " 从组合管理器发送给执行处理器以下单。\n", + "\n", + " This is a concrete order: it specifies WHAT to trade and HOW MUCH.\n", + " 这是一个具体订单:指定交易什么以及交易多少。\n", + "\n", + " Fields / 字段:\n", + " symbol 股票代码\n", + " date 下单日期\n", + " order_type 订单类型 MARKET / LIMIT / STOP\n", + " direction 方向 LONG / SHORT / EXIT\n", + " quantity 数量 number of shares (股数)\n", + " limit_price 限价 for LIMIT orders (限价单价格)\n", + " stop_price 止损价 for STOP orders (止损单价格)\n", + " \"\"\"\n", + " type : EventType = field(default=EventType.ORDER, init=False)\n", + " symbol : str = \"\"\n", + " date : pd.Timestamp = None\n", + " order_type : OrderType = OrderType.MARKET\n", + " direction : Direction = Direction.LONG\n", + " quantity : int = 0\n", + " limit_price : float = 0.0\n", + " stop_price : float = 0.0\n", + "\n", + " def __repr__(self):\n", + " return (f\"OrderEvent({self.symbol} {self.date.date()} \"\n", + " f\"{self.order_type.value} {self.direction.value} \"\n", + " f\"qty={self.quantity})\")\n", + "\n", + "\n", + "@dataclass\n", + "class FillEvent:\n", + " \"\"\"\n", + " Confirms that an order has been executed by the broker.\n", + " 确认订单已被券商执行(成交回报)。\n", + "\n", + " A Fill is the ground truth — it records what ACTUALLY happened.\n", + " 成交回报是最终事实——记录实际发生了什么。\n", + "\n", + " Fields / 字段:\n", + " symbol 股票代码\n", + " date 成交日期\n", + " direction 成交方向 LONG / SHORT / EXIT\n", + " quantity 成交数量 shares actually filled\n", + " fill_price 成交价格 actual execution price (含滑点 including slippage)\n", + " commission 佣金 brokerage commission\n", + " slippage 滑点 price impact of the trade\n", + " \"\"\"\n", + " type : EventType = field(default=EventType.FILL, init=False)\n", + " symbol : str = \"\"\n", + " date : pd.Timestamp = None\n", + " direction : Direction = Direction.LONG\n", + " quantity : int = 0\n", + " fill_price : float = 0.0\n", + " commission : float = 0.0\n", + " slippage : float = 0.0\n", + "\n", + " @property\n", + " def total_cost(self) -> float:\n", + " \"\"\"\n", + " Total cash impact of this fill.\n", + " 本次成交的总现金影响。\n", + "\n", + " For a BUY (LONG): negative (cash decreases) 现金减少\n", + " For a SELL (EXIT): positive (cash increases) 现金增加\n", + " \"\"\"\n", + " sign = -1 if self.direction in (Direction.LONG, Direction.SHORT) else +1\n", + " return sign * (self.fill_price * self.quantity + self.commission)\n", + "\n", + " def __repr__(self):\n", + " return (f\"FillEvent({self.symbol} {self.date.date()} \"\n", + " f\"{self.direction.value} qty={self.quantity} \"\n", + " f\"@{self.fill_price:.2f} comm={self.commission:.2f})\")" + ] + }, + { + "cell_type": "markdown", + "id": "1cdf71ef", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 2: DataHandler 数据处理器\n", + "\n", + "# =============================================================================\n", + "#\n", + "# The DataHandler is responsible for feeding price data into the system\n", + "# one bar at a time. It simulates the reality of not knowing the future.\n", + "#\n", + "# 数据处理器负责逐根K线地向系统输入价格数据。\n", + "# 它模拟了\"无法预知未来\"的现实。\n", + "#\n", + "# The key design rule:\n", + "# 核心设计规则:\n", + "#\n", + "# ❌ WRONG: strategy sees bar[t] and trades on bar[t] (lookahead bias!)\n", + "# 策略看到 bar[t] 并在 bar[t] 交易(前视偏差!)\n", + "#\n", + "# ✅ RIGHT: strategy sees bar[t], generates signal based on bar[t],\n", + "# order fills at bar[t+1] open (next day open)\n", + "# 策略看到 bar[t],生成信号,订单在 bar[t+1] 的开盘价成交\n", + "#\n", + "# This is why DataHandler, Strategy, Portfolio, and ExecutionHandler are\n", + "# separate — the separation naturally enforces the time boundary.\n", + "# 这就是为什么这四个组件分开——分离自然地强制了时间边界。\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90210d19", + "metadata": {}, + "outputs": [], + "source": [ + "class DataHandler:\n", + " \"\"\"\n", + " Streams historical OHLCV data bar by bar into the event queue.\n", + " 将历史 OHLCV 数据逐根K线地流入事件队列。\n", + "\n", + " In live trading, this would connect to a real-time market data API.\n", + " 在实盘交易中,这里会连接实时行情 API。\n", + " \"\"\"\n", + "\n", + " def __init__(self, data: pd.DataFrame, symbol: str, event_queue: queue.Queue):\n", + " \"\"\"\n", + " Parameters / 参数:\n", + " data — DataFrame with DatetimeIndex and columns:\n", + " 带 DatetimeIndex 的 DataFrame,列名为:\n", + " open, high, low, close, volume\n", + " symbol — ticker symbol / 股票代码\n", + " event_queue — the shared event queue / 共享事件队列\n", + " \"\"\"\n", + " self.data = data.copy()\n", + " self.symbol = symbol\n", + " self.event_queue = event_queue\n", + " self._dates = list(data.index) # all trading dates\n", + " self._cursor = 0 # current position in history\n", + " self._history = [] # bars seen so far (已观测的K线)\n", + "\n", + " @property\n", + " def current_date(self) -> Optional[pd.Timestamp]:\n", + " \"\"\"The date of the most recently published bar. 最新已发布K线的日期。\"\"\"\n", + " if self._cursor == 0:\n", + " return None\n", + " return self._dates[self._cursor - 1]\n", + "\n", + " @property\n", + " def n_bars_seen(self) -> int:\n", + " \"\"\"How many bars have been streamed so far. 到目前为止已流出多少根K线。\"\"\"\n", + " return self._cursor\n", + "\n", + " def has_more_data(self) -> bool:\n", + " \"\"\"Returns True if there is at least one more bar to stream.\"\"\"\n", + " return self._cursor < len(self._dates)\n", + "\n", + " def stream_next(self) -> bool:\n", + " \"\"\"\n", + " Emit the next bar as a MarketEvent onto the queue.\n", + " 将下一根K线作为 MarketEvent 发送到队列。\n", + "\n", + " Returns True if a bar was emitted, False if data is exhausted.\n", + " 如果发出了K线则返回 True,如果数据已耗尽则返回 False。\n", + " \"\"\"\n", + " if not self.has_more_data():\n", + " return False\n", + "\n", + " date = self._dates[self._cursor]\n", + " row = self.data.loc[date]\n", + "\n", + " # Build the MarketEvent for this bar / 构建本根K线的 MarketEvent\n", + " bar = MarketEvent(\n", + " symbol = self.symbol,\n", + " date = date,\n", + " open = float(row.get(\"open\", row.get(\"close\", 0))),\n", + " high = float(row.get(\"high\", row.get(\"close\", 0))),\n", + " low = float(row.get(\"low\", row.get(\"close\", 0))),\n", + " close = float(row[\"close\"]),\n", + " volume = float(row.get(\"volume\", 0)),\n", + " )\n", + "\n", + " # Add to internal history (the strategy can query this)\n", + " # 添加到内部历史记录(策略可以查询)\n", + " self._history.append(bar)\n", + " self._cursor += 1\n", + "\n", + " # Push onto the central event queue / 推送到中央事件队列\n", + " self.event_queue.put(bar)\n", + " return True\n", + "\n", + " def get_history(self, n: Optional[int] = None) -> List[MarketEvent]:\n", + " \"\"\"\n", + " Return the last n bars seen so far (default: all history).\n", + " 返回迄今为止看到的最后 n 根K线(默认:全部历史)。\n", + "\n", + " This is what the Strategy uses to compute indicators.\n", + " 这是策略用于计算技术指标的数据。\n", + " \"\"\"\n", + " if n is None:\n", + " return self._history\n", + " return self._history[-n:]\n", + "\n", + " def get_close_series(self) -> pd.Series:\n", + " \"\"\"\n", + " Return a pd.Series of all close prices seen so far.\n", + " 返回到目前为止所有已观测收盘价的 pd.Series。\n", + "\n", + " Useful for computing rolling indicators (移动平均等滚动指标).\n", + " \"\"\"\n", + " if not self._history:\n", + " return pd.Series(dtype=float)\n", + " return pd.Series(\n", + " [b.close for b in self._history],\n", + " index=[b.date for b in self._history],\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "0982acc9", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 3: Strategy (Abstract Base + Concrete Implementations)\n", + "# 策略(抽象基类 + 具体实现)\n", + "\n", + "# =============================================================================\n", + "#\n", + "# A Strategy:\n", + "# 1. Listens for MarketEvents\n", + "# 2. Computes indicators on the historical data it has seen so far\n", + "# 3. Generates SignalEvents when its rules are triggered\n", + "#\n", + "# 策略:\n", + "# 1. 监听 MarketEvent\n", + "# 2. 根据迄今为止看到的历史数据计算指标\n", + "# 3. 当规则被触发时生成 SignalEvent\n", + "#\n", + "# The abstract base class (ABC) enforces a consistent interface so that\n", + "# we can swap strategies without changing any other component.\n", + "# 抽象基类强制统一接口,这样我们可以替换策略而不改变其他任何组件。\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1520845f", + "metadata": {}, + "outputs": [], + "source": [ + "class Strategy(ABC):\n", + " \"\"\"\n", + " Abstract base class for all strategies.\n", + " 所有策略的抽象基类。\n", + " \"\"\"\n", + "\n", + " def __init__(self, data_handler: DataHandler, event_queue: queue.Queue):\n", + " self.data = data_handler\n", + " self.queue = event_queue\n", + "\n", + " @abstractmethod\n", + " def on_market(self, event: MarketEvent) -> None:\n", + " \"\"\"\n", + " Called when a new MarketEvent arrives.\n", + " 新的 MarketEvent 到达时调用。\n", + " Subclasses implement their signal logic here.\n", + " 子类在这里实现信号逻辑。\n", + " \"\"\"\n", + " ...\n", + "\n", + " def send_signal(self, symbol: str, date: pd.Timestamp,\n", + " direction: Direction, strength: float = 1.0) -> None:\n", + " \"\"\"Helper to build and queue a SignalEvent. 构建并排队 SignalEvent 的辅助方法。\"\"\"\n", + " sig = SignalEvent(\n", + " symbol = symbol,\n", + " date = date,\n", + " direction = direction,\n", + " strength = strength,\n", + " strategy = self.__class__.__name__,\n", + " )\n", + " self.queue.put(sig)\n", + "\n", + "\n", + "class MACrossoverStrategy(Strategy):\n", + " \"\"\"\n", + " Dual Moving Average Crossover Strategy 双均线金叉/死叉策略\n", + " ─────────────────────────────────────────────────────────────\n", + " Rules / 规则:\n", + " Golden Cross (金叉): short SMA crosses ABOVE long SMA → BUY (做多)\n", + " Death Cross (死叉): short SMA crosses BELOW long SMA → SELL (平仓)\n", + "\n", + " Parameters / 参数:\n", + " short_window 短期均线窗口 (default 20 days)\n", + " long_window 长期均线窗口 (default 60 days)\n", + " \"\"\"\n", + "\n", + " def __init__(self, data_handler: DataHandler, event_queue: queue.Queue,\n", + " short_window: int = 20, long_window: int = 60):\n", + " super().__init__(data_handler, event_queue)\n", + " self.short_window = short_window\n", + " self.long_window = long_window\n", + " self._in_position = False # are we currently holding a long position?\n", + " # 当前是否持有多仓?\n", + "\n", + " def on_market(self, event: MarketEvent) -> None:\n", + " \"\"\"\n", + " Called on every new bar. 每根新K线调用一次。\n", + "\n", + " We need at least long_window bars before we can compute the long SMA.\n", + " 至少需要 long_window 根K线才能计算长期均线。\n", + " \"\"\"\n", + " closes = self.data.get_close_series()\n", + " n = len(closes)\n", + "\n", + " # Not enough data yet — wait / 数据不足,等待\n", + " if n < self.long_window:\n", + " return\n", + "\n", + " # Compute SMAs using only data available up to this bar\n", + " # 仅使用截至本K线的数据计算均线(无前视偏差)\n", + " sma_short = closes.iloc[-self.short_window:].mean()\n", + " sma_long = closes.iloc[-self.long_window:].mean()\n", + "\n", + " # Previous bar's SMAs (to detect crossover / 检测交叉)\n", + " if n < self.long_window + 1:\n", + " return # need one extra bar to detect a CHANGE in relationship\n", + "\n", + " sma_short_prev = closes.iloc[-(self.short_window + 1):-1].mean()\n", + " sma_long_prev = closes.iloc[-(self.long_window + 1):-1].mean()\n", + "\n", + " was_above = sma_short_prev > sma_long_prev # previous relationship\n", + " is_above = sma_short > sma_long # current relationship\n", + "\n", + " # ── Golden Cross 金叉 ─────────────────────────────────────────────\n", + " # Short MA just crossed ABOVE long MA → bullish signal\n", + " # 短期均线刚刚上穿长期均线 → 看涨信号\n", + " if is_above and not was_above and not self._in_position:\n", + " self.send_signal(event.symbol, event.date, Direction.LONG, strength=1.0)\n", + " self._in_position = True\n", + "\n", + " # ── Death Cross 死叉 ─────────────────────────────────────────────\n", + " # Short MA just crossed BELOW long MA → exit signal\n", + " # 短期均线刚刚下穿长期均线 → 平仓信号\n", + " elif not is_above and was_above and self._in_position:\n", + " self.send_signal(event.symbol, event.date, Direction.EXIT, strength=1.0)\n", + " self._in_position = False\n", + "\n", + "\n", + "class RSIMeanReversionStrategy(Strategy):\n", + " \"\"\"\n", + " RSI Mean Reversion Strategy RSI 均值回归策略\n", + " ─────────────────────────────────────────────\n", + " Rules / 规则:\n", + " RSI < oversold (超卖) → BUY (做多)\n", + " RSI > overbought(超买) → SELL EXIT (平多)\n", + " (Also supports going SHORT / 也支持做空)\n", + "\n", + " Parameters / 参数:\n", + " window RSI计算窗口 (default 14)\n", + " oversold 超卖阈值 (default 30)\n", + " overbought 超买阈值 (default 70)\n", + " \"\"\"\n", + "\n", + " def __init__(self, data_handler: DataHandler, event_queue: queue.Queue,\n", + " window: int = 14, oversold: float = 30, overbought: float = 70):\n", + " super().__init__(data_handler, event_queue)\n", + " self.window = window\n", + " self.oversold = oversold\n", + " self.overbought = overbought\n", + " self._position = 0 # 0=flat, 1=long, -1=short (0=空仓, 1=多仓, -1=空仓)\n", + "\n", + " def _compute_rsi(self, closes: pd.Series) -> float:\n", + " \"\"\"\n", + " Compute the current RSI value using Wilder's smoothing.\n", + " 使用 Wilder 平滑法计算当前 RSI 值。\n", + " \"\"\"\n", + " if len(closes) < self.window + 1:\n", + " return 50.0 # neutral / 中性,数据不足时返回中性值\n", + "\n", + " delta = closes.diff()\n", + " gain = delta.clip(lower=0)\n", + " loss = -delta.clip(upper=0)\n", + " avg_gain = gain.ewm(alpha=1.0 / self.window, adjust=False).mean().iloc[-1]\n", + " avg_loss = loss.ewm(alpha=1.0 / self.window, adjust=False).mean().iloc[-1]\n", + "\n", + " if avg_loss == 0:\n", + " return 100.0\n", + " rs = avg_gain / avg_loss\n", + " return 100.0 - (100.0 / (1.0 + rs))\n", + "\n", + " def on_market(self, event: MarketEvent) -> None:\n", + " closes = self.data.get_close_series()\n", + " rsi = self._compute_rsi(closes)\n", + "\n", + " # ── Entry: Oversold → Go Long 入场:超卖 → 做多 ──────────────────\n", + " if rsi < self.oversold and self._position == 0:\n", + " self.send_signal(event.symbol, event.date, Direction.LONG, strength=0.8)\n", + " self._position = 1\n", + "\n", + " # ── Entry: Overbought → Go Short 入场:超买 → 做空 ───────────────\n", + " elif rsi > self.overbought and self._position == 0:\n", + " self.send_signal(event.symbol, event.date, Direction.SHORT, strength=0.8)\n", + " self._position = -1\n", + "\n", + " # ── Exit Long when RSI recovers to neutral 多仓RSI回到中性时平仓 ──\n", + " elif self._position == 1 and rsi > 50:\n", + " self.send_signal(event.symbol, event.date, Direction.EXIT, strength=1.0)\n", + " self._position = 0\n", + "\n", + " # ── Exit Short when RSI falls back to neutral 空仓RSI回到中性时平仓\n", + " elif self._position == -1 and rsi < 50:\n", + " self.send_signal(event.symbol, event.date, Direction.EXIT, strength=1.0)\n", + " self._position = 0" + ] + }, + { + "cell_type": "markdown", + "id": "0256875a", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 4: Portfolio 组合管理器\n", + "\n", + "# =============================================================================\n", + "#\n", + "# The Portfolio is the \"brain\" that manages money. It:\n", + "# 1. Receives SignalEvents from the Strategy\n", + "# 2. Decides HOW MUCH to trade (position sizing / 仓位管理)\n", + "# 3. Creates OrderEvents for the ExecutionHandler\n", + "# 4. Receives FillEvents from the broker and updates:\n", + "# - Cash balance (现金余额)\n", + "# - Positions (持仓)\n", + "# - Realized P&L (已实现盈亏)\n", + "# - Unrealized P&L(未实现盈亏)\n", + "# 5. Records the daily equity (净值) for performance analysis\n", + "#\n", + "# 组合管理器是管理资金的\"大脑\"。它接收策略信号,决定交易多少,\n", + "# 向执行器发出订单,接收成交回报并更新现金/持仓/盈亏,记录每日净值。\n", + "#\n", + "# Position Sizing (仓位管理) methods / 常见方法:\n", + "# Fixed Fraction 固定比例 — always use X% of capital (简单,本文采用)\n", + "# Fixed Dollar 固定金额 — always trade $N worth\n", + "# Kelly Criterion 凯利公式 — optimal fraction based on edge & odds\n", + "# Volatility Scaling 波动率缩放 — scale size to target a fixed daily risk\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22482bd2", + "metadata": {}, + "outputs": [], + "source": [ + "class Portfolio:\n", + " \"\"\"\n", + " Tracks cash, positions, and P&L. Records equity curve.\n", + " 追踪现金、持仓和盈亏,记录净值曲线。\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " data_handler : DataHandler,\n", + " event_queue : queue.Queue,\n", + " initial_capital: float = 1_000_000.0,\n", + " position_pct : float = 0.95, # fraction of capital to deploy per trade\n", + " # 每次交易使用资本的比例 (95%)\n", + " ):\n", + " self.data = data_handler\n", + " self.queue = event_queue\n", + " self.initial_capital = initial_capital\n", + " self.position_pct = position_pct\n", + "\n", + " # ── Account state 账户状态 ──────────────────────────────────────\n", + " self.cash = initial_capital # 现金余额 / available cash\n", + " self.positions : Dict[str, int] = {} # symbol → qty (持仓数量)\n", + " self.avg_cost : Dict[str, float] = {} # symbol → avg entry price (持仓均价)\n", + "\n", + " # ── Trade log 交易记录 ───────────────────────────────────────────\n", + " # Every completed fill gets recorded here for analysis.\n", + " # 每笔完成的成交都记录在这里以供分析。\n", + " self.trade_log: List[dict] = []\n", + "\n", + " # ── Equity curve 净值曲线 ────────────────────────────────────────\n", + " # Recorded once per day after market close.\n", + " # 每天收盘后记录一次。\n", + " self.equity_curve: List[dict] = []\n", + "\n", + " # ── Computed properties 计算属性 ─────────────────────────────────────────\n", + "\n", + " def market_value(self, current_prices: Dict[str, float]) -> float:\n", + " \"\"\"\n", + " Current market value of all open positions.\n", + " 所有开放持仓的当前市值。\n", + " 市值 = Σ(持仓数量 × 当前价格)\n", + " \"\"\"\n", + " return sum(\n", + " qty * current_prices.get(sym, 0.0)\n", + " for sym, qty in self.positions.items()\n", + " )\n", + "\n", + " def total_equity(self, current_prices: Dict[str, float]) -> float:\n", + " \"\"\"\n", + " Total portfolio value: cash + market value of positions.\n", + " 账户总价值:现金 + 持仓市值。\n", + " 总资产 = 现金余额 + 持仓市值\n", + " \"\"\"\n", + " return self.cash + self.market_value(current_prices)\n", + "\n", + " # ── Event handlers 事件处理器 ─────────────────────────────────────────────\n", + "\n", + " def on_signal(self, event: SignalEvent) -> None:\n", + " \"\"\"\n", + " Convert a SignalEvent into an OrderEvent.\n", + " 将 SignalEvent 转换为 OrderEvent(决定下多少单)。\n", + "\n", + " This is where position sizing (仓位管理) happens.\n", + " 这里进行仓位计算。\n", + " \"\"\"\n", + " symbol = event.symbol\n", + " direction = event.direction\n", + "\n", + " # ── Determine quantity 计算交易数量 ─────────────────────────────\n", + " # We use Fixed Fraction sizing: deploy position_pct of current equity.\n", + " # 使用固定比例法:每次使用当前总资产的 position_pct。\n", + " #\n", + " # quantity = floor( equity × position_pct × strength / current_price )\n", + " # 数量 = floor( 总资产 × 仓位比例 × 信号强度 / 当前价格 )\n", + "\n", + " current_price = self.data.get_close_series().iloc[-1]\n", + "\n", + " if direction == Direction.EXIT:\n", + " # Close the entire existing position / 平掉全部现有持仓\n", + " qty = abs(self.positions.get(symbol, 0))\n", + " if qty == 0:\n", + " return # nothing to close / 没有持仓可平\n", + " else:\n", + " # New entry — compute share count / 新入场——计算股数\n", + " capital_to_deploy = self.cash * self.position_pct * event.strength\n", + " qty = int(capital_to_deploy / current_price)\n", + " if qty == 0:\n", + " return # not enough cash / 资金不足\n", + "\n", + " order = OrderEvent(\n", + " symbol = symbol,\n", + " date = event.date,\n", + " order_type = OrderType.MARKET, # use market orders for simplicity\n", + " direction = direction,\n", + " quantity = qty,\n", + " )\n", + " self.queue.put(order)\n", + "\n", + " def on_fill(self, event: FillEvent) -> None:\n", + " \"\"\"\n", + " Update cash and positions when a fill is confirmed.\n", + " 成交确认后更新现金和持仓。\n", + " \"\"\"\n", + " symbol = event.symbol\n", + " qty = event.quantity\n", + " price = event.fill_price\n", + " comm = event.commission\n", + "\n", + " if event.direction in (Direction.LONG, Direction.SHORT):\n", + " # ── Opening a position 建仓 ──────────────────────────────────\n", + " sign = 1 if event.direction == Direction.LONG else -1\n", + "\n", + " # Update average cost 更新持仓均价\n", + " old_qty = self.positions.get(symbol, 0)\n", + " old_cost = self.avg_cost.get(symbol, 0.0)\n", + " new_qty = old_qty + sign * qty\n", + " if new_qty != 0:\n", + " # Weighted average cost 加权平均成本\n", + " self.avg_cost[symbol] = (\n", + " (old_cost * abs(old_qty) + price * qty) / abs(new_qty)\n", + " )\n", + " self.positions[symbol] = new_qty\n", + "\n", + " # Deduct cash (buy) / add cash (short sell)\n", + " # 扣除现金(买入)/ 增加现金(卖空)\n", + " self.cash -= sign * (price * qty) + comm\n", + "\n", + " elif event.direction == Direction.EXIT:\n", + " # ── Closing a position 平仓 ──────────────────────────────────\n", + " entry_price = self.avg_cost.get(symbol, price)\n", + " pos_sign = 1 if self.positions.get(symbol, 0) > 0 else -1\n", + "\n", + " # Realized P&L 已实现盈亏\n", + " # P&L = (exit_price - entry_price) × qty × direction\n", + " pnl = pos_sign * (price - entry_price) * qty - comm\n", + "\n", + " # Return cash from closing the position\n", + " # 平仓回收现金\n", + " self.cash += pos_sign * price * qty - comm\n", + "\n", + " # Clear position 清除持仓\n", + " self.positions[symbol] = 0\n", + " self.avg_cost[symbol] = 0.0\n", + "\n", + " # Log the trade 记录交易\n", + " self.trade_log.append({\n", + " \"date\" : event.date,\n", + " \"symbol\" : symbol,\n", + " \"direction\" : \"LONG→EXIT\" if pos_sign == 1 else \"SHORT→EXIT\",\n", + " \"entry_price\" : entry_price,\n", + " \"exit_price\" : price,\n", + " \"quantity\" : qty,\n", + " \"pnl\" : pnl,\n", + " \"commission\" : comm,\n", + " })\n", + "\n", + " def record_equity(self, date: pd.Timestamp, current_price: float) -> None:\n", + " \"\"\"\n", + " Snapshot the portfolio value at end of day.\n", + " 记录每日收盘时的组合价值快照。\n", + " \"\"\"\n", + " prices = {self.data.symbol: current_price}\n", + " self.equity_curve.append({\n", + " \"date\" : date,\n", + " \"cash\" : self.cash,\n", + " \"market_value\" : self.market_value(prices),\n", + " \"total_equity\" : self.total_equity(prices),\n", + " })\n", + "\n", + " def get_equity_series(self) -> pd.Series:\n", + " \"\"\"Return equity curve as a pd.Series. 返回净值曲线为 pd.Series。\"\"\"\n", + " if not self.equity_curve:\n", + " return pd.Series(dtype=float)\n", + " df = pd.DataFrame(self.equity_curve).set_index(\"date\")\n", + " return df[\"total_equity\"]" + ] + }, + { + "cell_type": "markdown", + "id": "34df6025", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 5: ExecutionHandler (Simulated Broker)\n", + "# 执行处理器(模拟券商)\n", + "\n", + "# =============================================================================\n", + "#\n", + "# In live trading, this connects to a real broker API (e.g. Interactive\n", + "# Brokers, Alpaca, or a Chinese broker via vnpy).\n", + "# 在实盘交易中,这里连接真实券商 API(如 Interactive Brokers、Alpaca\n", + "# 或通过 vnpy 连接国内券商)。\n", + "#\n", + "# In backtesting, we SIMULATE the broker. The key costs to model are:\n", + "# 在回测中,我们模拟券商。需要建模的主要成本:\n", + "#\n", + "# 1. Commission 佣金\n", + "# A fixed percentage of trade value charged by the broker.\n", + "# 券商按交易金额收取的固定比例费用。\n", + "# Typical in China A-shares: 万分之三 (0.03%) per side\n", + "# 中国A股典型佣金:单边万分之三\n", + "#\n", + "# 2. Slippage 滑点\n", + "# The difference between the price at signal time and actual fill price.\n", + "# 信号生成时的价格与实际成交价格之间的差异。\n", + "# Causes: bid-ask spread, market impact, order queue position.\n", + "# 原因:买卖价差、市场冲击、排队等待。\n", + "#\n", + "# Slippage models / 滑点模型:\n", + "# Fixed percentage 固定比例 — simple, conservative\n", + "# Volume-weighted 成交量加权 — larger orders → more slippage\n", + "# Fixed spread 固定点差 — realistic for liquid markets\n", + "#\n", + "# 3. Fill Assumption 成交假设\n", + "# We assume market orders fill at next bar's OPEN price.\n", + "# 我们假设市价单在下一根K线的开盘价成交。\n", + "# This is a conservative and common assumption.\n", + "# 这是保守且常用的假设。\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a0af244c", + "metadata": {}, + "outputs": [], + "source": [ + "class SimulatedBroker:\n", + " \"\"\"\n", + " Simulates a brokerage: fills market orders with realistic costs.\n", + " 模拟券商:以真实成本成交市价单。\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " event_queue : queue.Queue,\n", + " commission_rate : float = 0.0003, # 0.03% per side / 单边万分之三\n", + " slippage_rate : float = 0.0001, # 0.01% slippage / 万分之一滑点\n", + " fill_on : str = \"next_open\", # when to fill / 何时成交\n", + " min_commission : float = 5.0, # minimum commission / 最低佣金\n", + " ):\n", + " \"\"\"\n", + " Parameters / 参数:\n", + " commission_rate 佣金率 (per side / 单边)\n", + " slippage_rate 滑点率 (applied as adverse price move / 不利价格移动)\n", + " fill_on 成交时机 'next_open' or 'current_close'\n", + " min_commission 最低佣金\n", + " \"\"\"\n", + " self.queue = event_queue\n", + " self.commission_rate = commission_rate\n", + " self.slippage_rate = slippage_rate\n", + " self.fill_on = fill_on\n", + " self.min_commission = min_commission\n", + "\n", + " # Store pending orders to fill on next bar's open\n", + " # 存储待处理订单,在下一根K线开盘价成交\n", + " self._pending_orders: List[OrderEvent] = []\n", + "\n", + " def on_order(self, event: OrderEvent) -> None:\n", + " \"\"\"\n", + " Receive an order from Portfolio.\n", + " 接收来自组合管理器的订单。\n", + "\n", + " For market orders, we queue them to fill at next open.\n", + " 对于市价单,我们将其排队,在下一根K线的开盘价成交。\n", + " \"\"\"\n", + " if event.order_type == OrderType.MARKET:\n", + " self._pending_orders.append(event)\n", + " # Limit and Stop orders would need price monitoring — not implemented here\n", + " # 限价单和止损单需要价格监控——此处未实现,留作扩展\n", + "\n", + " def fill_pending(self, bar: MarketEvent) -> None:\n", + " \"\"\"\n", + " Fill all pending orders at the open of the provided bar.\n", + " 以提供K线的开盘价成交所有待处理订单。\n", + "\n", + " This is called at the start of each new bar, before the Strategy\n", + " gets to process it.\n", + " 每根新K线开始时(策略处理之前)调用。\n", + " \"\"\"\n", + " for order in self._pending_orders:\n", + " fill_price = self._compute_fill_price(order, bar)\n", + " commission = self._compute_commission(order, fill_price)\n", + " slippage = fill_price - bar.open # adverse difference from open\n", + "\n", + " fill = FillEvent(\n", + " symbol = order.symbol,\n", + " date = bar.date, # filled on THIS bar's date\n", + " direction = order.direction,\n", + " quantity = order.quantity,\n", + " fill_price = fill_price,\n", + " commission = commission,\n", + " slippage = slippage,\n", + " )\n", + " self.queue.put(fill)\n", + "\n", + " self._pending_orders.clear()\n", + "\n", + " def _compute_fill_price(self, order: OrderEvent, bar: MarketEvent) -> float:\n", + " \"\"\"\n", + " Apply slippage to the open price.\n", + " 对开盘价施加滑点。\n", + "\n", + " Slippage always works AGAINST the trader:\n", + " 滑点总是对交易者不利:\n", + " BUY (LONG) : fill above open (买入时成交价高于开盘价)\n", + " SELL (EXIT) : fill below open (卖出时成交价低于开盘价)\n", + " \"\"\"\n", + " direction = order.direction\n", + " if direction in (Direction.LONG, Direction.SHORT):\n", + " # Buying: slippage pushes price UP 买入:滑点使价格上升\n", + " return bar.open * (1 + self.slippage_rate)\n", + " else:\n", + " # Selling: slippage pushes price DOWN 卖出:滑点使价格下降\n", + " return bar.open * (1 - self.slippage_rate)\n", + "\n", + " def _compute_commission(self, order: OrderEvent, fill_price: float) -> float:\n", + " \"\"\"\n", + " Compute brokerage commission.\n", + " 计算券商佣金。\n", + "\n", + " commission = max(commission_rate × trade_value, min_commission)\n", + " 佣金 = max(佣金率 × 交易金额, 最低佣金)\n", + " \"\"\"\n", + " trade_value = fill_price * order.quantity\n", + " commission = max(trade_value * self.commission_rate, self.min_commission)\n", + " return commission" + ] + }, + { + "cell_type": "markdown", + "id": "aa2f0b07", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 6: BacktestEngine 回测引擎(主事件循环)\n", + "\n", + "# =============================================================================\n", + "#\n", + "# The BacktestEngine is the conductor — it orchestrates all components by\n", + "# running the main event loop.\n", + "# 回测引擎是指挥者——通过运行主事件循环来协调所有组件。\n", + "#\n", + "# Main Loop Algorithm 主循环算法:\n", + "# ──────────────────────────────────────────────────────────────────────────\n", + "# WHILE data feed has more bars:\n", + "# 1. DataHandler.stream_next() → puts MarketEvent on queue\n", + "# 2. Broker.fill_pending(bar) → fills any leftover orders FIRST\n", + "# (orders placed yesterday fill at today's open — no lookahead!)\n", + "# (昨天的订单今天开盘价成交 — 无前视偏差!)\n", + "# 3. WHILE queue is not empty:\n", + "# event = queue.get()\n", + "# IF MarketEvent → Strategy.on_market(event)\n", + "# ELIF SignalEvent → Portfolio.on_signal(event)\n", + "# ELIF OrderEvent → Broker.on_order(event)\n", + "# ELIF FillEvent → Portfolio.on_fill(event)\n", + "# 4. Portfolio.record_equity(today's close)\n", + "# END WHILE\n", + "# ──────────────────────────────────────────────────────────────────────────\n", + "#\n", + "# Why is step 2 (fill_pending) before step 3 (process queue)?\n", + "# 为什么步骤2(成交待处理)在步骤3(处理队列)之前?\n", + "#\n", + "# Orders generated on day T are pending. On day T+1 the broker fills them\n", + "# at the open BEFORE the strategy sees day T+1's data.\n", + "# T日生成的订单是待处理状态。T+1日的开盘价成交(在策略看到T+1数据之前)。\n", + "# This correctly models the one-day execution delay of real trading.\n", + "# 这正确模拟了真实交易的一天执行延迟。\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "735f99e9", + "metadata": {}, + "outputs": [], + "source": [ + "class BacktestEngine:\n", + " \"\"\"\n", + " Main event-driven backtest engine. 主事件驱动回测引擎。\n", + " Wires together DataHandler, Strategy, Portfolio, and Broker.\n", + " 将 DataHandler、Strategy、Portfolio 和 Broker 组合在一起。\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " data_handler : DataHandler,\n", + " strategy : Strategy,\n", + " portfolio : Portfolio,\n", + " broker : SimulatedBroker,\n", + " name : str = \"Event-Driven Backtest\",\n", + " ):\n", + " self.data = data_handler\n", + " self.strategy = strategy\n", + " self.portfolio = portfolio\n", + " self.broker = broker\n", + " self.name = name\n", + "\n", + " # All components share this single event queue / 所有组件共享这一事件队列\n", + " assert (data_handler.event_queue is strategy.queue is\n", + " portfolio.queue is broker.queue), \\\n", + " \"All components must share the same event queue!\"\n", + "\n", + " self._queue = data_handler.event_queue\n", + "\n", + " def run(self, verbose: bool = False) -> None:\n", + " \"\"\"\n", + " Execute the backtest from start to finish.\n", + " 从头到尾执行回测。\n", + "\n", + " Parameters / 参数:\n", + " verbose — if True, print every event (useful for debugging)\n", + " 如果为 True,打印每个事件(用于调试)\n", + " \"\"\"\n", + " bar_count = 0\n", + " event_count= 0\n", + "\n", + " print(f\"\\n{'─' * 60}\")\n", + " print(f\" 回测开始: {self.name}\")\n", + " print(f\" Backtest Start: {self.data.data.index[0].date()} → \"\n", + " f\"{self.data.data.index[-1].date()}\")\n", + " print(f\"{'─' * 60}\")\n", + "\n", + " # ── Main loop 主循环 ─────────────────────────────────────────────\n", + " while self.data.has_more_data():\n", + "\n", + " # ── Step 1: Stream the next bar 流出下一根K线 ─────────────────\n", + " # This puts a MarketEvent on the queue.\n", + " # 这会将一个 MarketEvent 放入队列。\n", + " self.data.stream_next()\n", + " bar_count += 1\n", + "\n", + " # ── Step 2: Fill yesterday's pending orders at today's open ───\n", + " # ── 步骤2:以今天的开盘价成交昨天的待处理订单 ─────────────────\n", + " current_bar = self.data._history[-1]\n", + " self.broker.fill_pending(current_bar)\n", + "\n", + " # ── Step 3: Process all events in the queue 处理队列中的所有事件\n", + " while not self._queue.empty():\n", + " event = self._queue.get()\n", + " event_count += 1\n", + "\n", + " if verbose:\n", + " print(f\" [{event.type.value}] {event}\")\n", + "\n", + " if event.type == EventType.MARKET:\n", + " # Strategy sees the new price bar and may emit a signal\n", + " # 策略看到新K线,可能发出信号\n", + " self.strategy.on_market(event)\n", + "\n", + " elif event.type == EventType.SIGNAL:\n", + " # Portfolio receives signal and may emit an order\n", + " # 组合管理器接收信号,可能发出订单\n", + " self.portfolio.on_signal(event)\n", + "\n", + " elif event.type == EventType.ORDER:\n", + " # Broker receives the order (will fill on next bar's open)\n", + " # 券商接收订单(将在下一根K线的开盘价成交)\n", + " self.broker.on_order(event)\n", + "\n", + " elif event.type == EventType.FILL:\n", + " # Portfolio updates its cash & position records\n", + " # 组合管理器更新现金和持仓记录\n", + " self.portfolio.on_fill(event)\n", + "\n", + " # ── Step 4: Record end-of-day equity 记录当日收盘净值 ───────\n", + " self.portfolio.record_equity(current_bar.date, current_bar.close)\n", + "\n", + " n_trades = len(self.portfolio.trade_log)\n", + " print(f\" 回测完成! Backtest Complete!\")\n", + " print(f\" 总K线数: {bar_count} 总事件数: {event_count} 总交易数: {n_trades}\")\n", + " print(f\"{'─' * 60}\")\n", + "\n", + " # ── Performance metrics 绩效指标 ─────────────────────────────────────────\n", + "\n", + " def metrics(self) -> dict:\n", + " \"\"\"\n", + " Compute performance metrics from the equity curve.\n", + " 从净值曲线计算绩效指标。\n", + " \"\"\"\n", + " eq = self.portfolio.get_equity_series()\n", + " ret = eq.pct_change().fillna(0)\n", + " n = len(ret)\n", + " years = n / 252.0\n", + "\n", + " total_return = (eq.iloc[-1] / self.portfolio.initial_capital) - 1\n", + " cagr = (1 + total_return) ** (1 / years) - 1 if years > 0 else 0\n", + " ann_vol = ret.std() * np.sqrt(252)\n", + " sharpe = (ret.mean() / ret.std()) * np.sqrt(252) if ret.std() > 0 else 0\n", + "\n", + " downside_std = ret[ret < 0].std() * np.sqrt(252)\n", + " sortino = cagr / downside_std if downside_std > 0 else 0\n", + "\n", + " rolling_max = eq.cummax()\n", + " drawdown = (eq - rolling_max) / rolling_max\n", + " max_dd = drawdown.min()\n", + " calmar = cagr / abs(max_dd) if max_dd != 0 else 0\n", + " win_rate = (ret > 0).mean()\n", + "\n", + " # Trade-level statistics 交易级别统计\n", + " trades = self.portfolio.trade_log\n", + " n_trades = len(trades)\n", + " if n_trades > 0:\n", + " pnls = [t[\"pnl\"] for t in trades]\n", + " win_trades = [p for p in pnls if p > 0]\n", + " loss_trades= [p for p in pnls if p < 0]\n", + " trade_win_rate = len(win_trades) / n_trades\n", + " avg_win = np.mean(win_trades) if win_trades else 0\n", + " avg_loss = np.mean(loss_trades) if loss_trades else 0\n", + " profit_factor = (sum(win_trades) / abs(sum(loss_trades))\n", + " if loss_trades else np.inf)\n", + " else:\n", + " trade_win_rate = profit_factor = avg_win = avg_loss = 0\n", + "\n", + " return {\n", + " \"总收益率 Total Return\" : f\"{total_return:.2%}\",\n", + " \"年化收益率 CAGR\" : f\"{cagr:.2%}\",\n", + " \"年化波动率 Ann. Volatility\" : f\"{ann_vol:.2%}\",\n", + " \"夏普比率 Sharpe Ratio\" : f\"{sharpe:.3f}\",\n", + " \"索提诺比率 Sortino Ratio\" : f\"{sortino:.3f}\",\n", + " \"最大回撤 Max Drawdown\" : f\"{max_dd:.2%}\",\n", + " \"卡玛比率 Calmar Ratio\" : f\"{calmar:.3f}\",\n", + " \"日胜率 Daily Win Rate\" : f\"{win_rate:.2%}\",\n", + " \"交易次数 # Trades (round trip)\": str(n_trades),\n", + " \"交易胜率 Trade Win Rate\" : f\"{trade_win_rate:.2%}\",\n", + " \"平均盈利 Avg Win (per trade)\" : f\"¥{avg_win:,.0f}\",\n", + " \"平均亏损 Avg Loss (per trade)\": f\"¥{avg_loss:,.0f}\",\n", + " \"盈亏比 Profit Factor\" : f\"{profit_factor:.3f}\",\n", + " }\n", + "\n", + " def print_metrics(self):\n", + " \"\"\"Pretty-print the performance report. 格式化打印绩效报告。\"\"\"\n", + " print(f\"\\n{'=' * 60}\")\n", + " print(f\" 策略绩效报告 / Performance Report\")\n", + " print(f\" {self.name}\")\n", + " print(f\"{'=' * 60}\")\n", + " for k, v in self.metrics().items():\n", + " print(f\" {k:<40} {v}\")\n", + "\n", + " print(f\"\\n ── 最近 5 笔交易 Last 5 Trades ────────────────────────\")\n", + " trades = self.portfolio.trade_log[-5:]\n", + " if trades:\n", + " for t in trades:\n", + " pnl_sign = \"+\" if t[\"pnl\"] >= 0 else \"\"\n", + " print(f\" {t['date'].date()} {t['direction']:<12} \"\n", + " f\"进价 {t['entry_price']:>8.2f} 出价 {t['exit_price']:>8.2f} \"\n", + " f\"股数 {t['quantity']:>5} \"\n", + " f\"盈亏 {pnl_sign}{t['pnl']:>10,.0f}¥\")\n", + " else:\n", + " print(\" (无完整交易 / No completed round-trip trades)\")\n", + " print(f\"{'=' * 60}\")" + ] + }, + { + "cell_type": "markdown", + "id": "1bf6c24a", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 7: Assemble & Run 组装并运行\n", + "\n", + "# =============================================================================\n", + "\n", + "# ── Generate synthetic price data (same seed as previous demo for consistency)\n", + "# ── 生成合成价格数据(与前一个演示相同的随机种子以保持一致性)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcb04ce6", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_price_series(n_days=1500, mu=0.10, sigma=0.25, s0=100.0, seed=42):\n", + " \"\"\"Geometric Brownian Motion price series. 几何布朗运动价格序列。\"\"\"\n", + " np.random.seed(seed)\n", + " dt = 1.0 / 252\n", + " eps = np.random.randn(n_days)\n", + " log_ret = (mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * eps\n", + " prices = s0 * np.exp(np.cumsum(log_ret))\n", + " dates = pd.bdate_range(start=\"2019-01-02\", periods=n_days)\n", + "\n", + " # Build a realistic OHLCV DataFrame 构建真实的 OHLCV DataFrame\n", + " daily_range = prices * np.abs(np.random.randn(n_days)) * 0.015\n", + " opens = prices * (1 + np.random.randn(n_days) * 0.003)\n", + " highs = np.maximum(prices, opens) + np.abs(np.random.randn(n_days)) * daily_range * 0.5\n", + " lows = np.minimum(prices, opens) - np.abs(np.random.randn(n_days)) * daily_range * 0.5\n", + " vols = np.random.lognormal(mean=14.5, sigma=0.5, size=n_days).astype(int)\n", + "\n", + " return pd.DataFrame({\n", + " \"open\": opens, \"high\": highs, \"low\": lows,\n", + " \"close\": prices, \"volume\": vols,\n", + " }, index=dates)\n", + "\n", + "\n", + "ohlcv = generate_price_series()\n", + "SYMBOL = \"SIM_STOCK\"\n", + "\n", + "print(f\"\\n[数据] 生成模拟 OHLCV 数据:\")\n", + "print(f\" 交易日数: {len(ohlcv)}\")\n", + "print(f\" 日期范围: {ohlcv.index[0].date()} → {ohlcv.index[-1].date()}\")\n", + "print(f\" 价格区间: {ohlcv['close'].min():.2f} ~ {ohlcv['close'].max():.2f}\")\n", + "\n", + "\n", + "def build_engine(strategy_class, strategy_kwargs: dict,\n", + " name: str, commission=0.0003, slippage=0.0001\n", + " ) -> BacktestEngine:\n", + " \"\"\"\n", + " Factory function: creates a fresh, independent backtest engine.\n", + " 工厂函数:创建一个全新的、独立的回测引擎。\n", + " 每次回测共享同样的价格数据,但所有组件(事件队列、组合、策略)\n", + " 都是独立实例,相互不干扰。\n", + " \"\"\"\n", + " q = queue.Queue() # fresh queue / 全新事件队列\n", + " data = DataHandler(ohlcv, SYMBOL, q) # streams OHLCV bar by bar\n", + " strategy = strategy_class(data, q, **strategy_kwargs)\n", + " portfolio = Portfolio(data, q, initial_capital=1_000_000.0)\n", + " broker = SimulatedBroker(q, commission_rate=commission,\n", + " slippage_rate=slippage)\n", + " return BacktestEngine(data, strategy, portfolio, broker, name=name)\n", + "\n", + "\n", + "# ── Run MA Crossover 运行双均线策略 ──────────────────────────────────────────\n", + "engine_ma = build_engine(\n", + " MACrossoverStrategy,\n", + " {\"short_window\": 20, \"long_window\": 60},\n", + " name=\"事件驱动 — 双均线策略 (MA Crossover 20/60)\",\n", + ")\n", + "engine_ma.run(verbose=False)\n", + "engine_ma.print_metrics()\n", + "\n", + "# ── Run RSI Mean Reversion 运行RSI均值回归策略 ──────────────────────────────\n", + "engine_rsi = build_engine(\n", + " RSIMeanReversionStrategy,\n", + " {\"window\": 14, \"oversold\": 30, \"overbought\": 70},\n", + " name=\"事件驱动 — RSI均值回归 (RSI Mean Reversion 14/30/70)\",\n", + ")\n", + "engine_rsi.run(verbose=False)\n", + "engine_rsi.print_metrics()" + ] + }, + { + "cell_type": "markdown", + "id": "d1e2b409", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 8: Visualization 可视化\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "460f465c", + "metadata": {}, + "outputs": [], + "source": [ + "fig = plt.figure(figsize=(16, 20))\n", + "gs = gridspec.GridSpec(5, 2, figure=fig, hspace=0.5, wspace=0.32)\n", + "\n", + "price_series = ohlcv[\"close\"]\n", + "\n", + "# ── Plot 1 (top, full width): Price + MA signals\n", + "# ── 图1(顶部,全宽): 价格 + 均线信号\n", + "ax1 = fig.add_subplot(gs[0, :])\n", + "ax1.plot(price_series, color=\"#1f77b4\", linewidth=1, alpha=0.9, label=\"价格 Close\")\n", + "sma20 = price_series.rolling(20).mean()\n", + "sma60 = price_series.rolling(60).mean()\n", + "ax1.plot(sma20, color=\"orange\", linewidth=1.3, label=\"SMA20 (快线)\")\n", + "ax1.plot(sma60, color=\"crimson\", linewidth=1.3, label=\"SMA60 (慢线)\")\n", + "\n", + "# Mark trade entries / exits from the event-driven engine\n", + "# 标记事件驱动引擎的交易进出场点\n", + "for trade in engine_ma.portfolio.trade_log:\n", + " color = \"green\" if \"LONG\" in trade[\"direction\"] else \"red\"\n", + " ax1.axvline(x=trade[\"date\"], color=color, alpha=0.25, linewidth=0.8)\n", + "\n", + "ax1.set_title(\"双均线策略 — 价格与信号 (MA Crossover: Price + Signals)\",\n", + " fontsize=12, fontweight=\"bold\")\n", + "ax1.legend(loc=\"upper left\", fontsize=9)\n", + "ax1.set_ylabel(\"价格 Price\")\n", + "ax1.grid(alpha=0.3)\n", + "\n", + "# ── Plot 2 (full width): Event flow detail for one trade\n", + "# ── 图2(全宽): 一笔交易的事件流细节(放大展示)\n", + "ax2 = fig.add_subplot(gs[1, :])\n", + "# Show a 120-day window around the first trade to illustrate the event flow\n", + "# 展示第一笔交易前后120天的窗口,说明事件流\n", + "if engine_ma.portfolio.trade_log:\n", + " trade0 = engine_ma.portfolio.trade_log[0]\n", + " focus_date = trade0[\"date\"]\n", + " window = pd.Timedelta(days=90)\n", + " mask = (price_series.index >= focus_date - window) & \\\n", + " (price_series.index <= focus_date + window)\n", + " ax2.plot(price_series[mask], color=\"#1f77b4\", linewidth=1.5)\n", + " ax2.plot(sma20[mask], color=\"orange\", linewidth=1.2, linestyle=\"--\", label=\"SMA20\")\n", + " ax2.plot(sma60[mask], color=\"crimson\", linewidth=1.2, linestyle=\"--\", label=\"SMA60\")\n", + " # Mark the exit point\n", + " ax2.axvline(x=focus_date, color=\"red\", linewidth=1.5,\n", + " label=f\"平仓 Exit @ {focus_date.date()}\")\n", + " ax2.set_title(\n", + " f\"放大: 第1笔交易前后 90 天 \"\n", + " f\"(Zoom: 90 days around Trade #1 Exit)\\n\"\n", + " f\"进价 Entry ¥{trade0['entry_price']:.2f} \"\n", + " f\"出价 Exit ¥{trade0['exit_price']:.2f} \"\n", + " f\"盈亏 P&L ¥{trade0['pnl']:,.0f}\",\n", + " fontsize=10, fontweight=\"bold\",\n", + " )\n", + " ax2.legend(fontsize=9)\n", + " ax2.grid(alpha=0.3)\n", + "\n", + "# ── Plot 3 (left): Equity curves 净值曲线\n", + "ax3 = fig.add_subplot(gs[2, 0])\n", + "eq_ma = engine_ma.portfolio.get_equity_series()\n", + "eq_rsi = engine_rsi.portfolio.get_equity_series()\n", + "eq_bh = 1_000_000 * (1 + price_series.pct_change().fillna(0)).cumprod()\n", + "\n", + "ax3.plot(eq_ma, color=\"steelblue\", linewidth=1.5, label=\"MA策略\")\n", + "ax3.plot(eq_rsi, color=\"purple\", linewidth=1.5, label=\"RSI策略\")\n", + "ax3.plot(eq_bh, color=\"gray\", linewidth=1.2, linestyle=\"--\", label=\"Buy & Hold\")\n", + "ax3.set_title(\"净值曲线对比 (Equity Curves)\", fontsize=11, fontweight=\"bold\")\n", + "ax3.set_ylabel(\"账户价值 Portfolio Value\")\n", + "ax3.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"¥{x/1e4:.0f}万\"))\n", + "ax3.legend(fontsize=9)\n", + "ax3.grid(alpha=0.3)\n", + "\n", + "# ── Plot 4 (right): Drawdown 回撤曲线\n", + "ax4 = fig.add_subplot(gs[2, 1])\n", + "def compute_dd(eq):\n", + " return (eq - eq.cummax()) / eq.cummax()\n", + "\n", + "dd_ma = compute_dd(eq_ma)\n", + "dd_rsi = compute_dd(eq_rsi)\n", + "dd_bh = compute_dd(eq_bh)\n", + "\n", + "ax4.fill_between(dd_ma.index, dd_ma, 0, alpha=0.5, color=\"steelblue\", label=\"MA策略\")\n", + "ax4.fill_between(dd_rsi.index, dd_rsi, 0, alpha=0.4, color=\"purple\", label=\"RSI策略\")\n", + "ax4.fill_between(dd_bh.index, dd_bh, 0, alpha=0.3, color=\"gray\", label=\"Buy & Hold\")\n", + "ax4.set_title(\"回撤曲线 (Drawdowns)\", fontsize=11, fontweight=\"bold\")\n", + "ax4.set_ylabel(\"回撤 Drawdown\")\n", + "ax4.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"{x:.0%}\"))\n", + "ax4.legend(fontsize=9)\n", + "ax4.grid(alpha=0.3)\n", + "\n", + "# ── Plot 5 (left): Trade P&L distribution 交易盈亏分布\n", + "ax5 = fig.add_subplot(gs[3, 0])\n", + "pnls = [t[\"pnl\"] for t in engine_ma.portfolio.trade_log]\n", + "if pnls:\n", + " colors_bar = [\"green\" if p > 0 else \"red\" for p in pnls]\n", + " ax5.bar(range(len(pnls)), pnls, color=colors_bar, alpha=0.75, edgecolor=\"white\")\n", + " ax5.axhline(0, color=\"black\", linewidth=0.8)\n", + " ax5.set_title(f\"MA策略 — 每笔交易盈亏 (Per-Trade P&L)\\n\"\n", + " f\"共{len(pnls)}笔, 胜率 \"\n", + " f\"{sum(1 for p in pnls if p > 0)/len(pnls):.0%}\",\n", + " fontsize=10, fontweight=\"bold\")\n", + " ax5.set_xlabel(\"交易编号 Trade #\")\n", + " ax5.set_ylabel(\"盈亏 P&L (¥)\")\n", + " ax5.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"¥{x/1e4:.1f}万\"))\n", + "ax5.grid(alpha=0.3, axis=\"y\")\n", + "\n", + "# ── Plot 6 (right): Cash vs Market Value over time\n", + "# ── 图6(右): 现金 vs 持仓市值随时间变化\n", + "ax6 = fig.add_subplot(gs[3, 1])\n", + "eq_df = pd.DataFrame(engine_ma.portfolio.equity_curve).set_index(\"date\")\n", + "ax6.stackplot(eq_df.index,\n", + " eq_df[\"cash\"], eq_df[\"market_value\"],\n", + " labels=[\"现金 Cash\", \"持仓市值 Market Value\"],\n", + " colors=[\"#2ca02c\", \"#ff7f0e\"], alpha=0.7)\n", + "ax6.set_title(\"现金 vs 持仓市值 (Cash vs Position Value)\", fontsize=11, fontweight=\"bold\")\n", + "ax6.set_ylabel(\"金额 Amount (¥)\")\n", + "ax6.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"¥{x/1e4:.0f}万\"))\n", + "ax6.legend(loc=\"upper left\", fontsize=9)\n", + "ax6.grid(alpha=0.3)\n", + "\n", + "# ── Plot 7 (full width): Rolling metrics — Sharpe & Volatility\n", + "# ── 图7(全宽): 滚动指标 — 夏普比率 & 波动率\n", + "ax7a = fig.add_subplot(gs[4, 0])\n", + "ax7b = fig.add_subplot(gs[4, 1])\n", + "\n", + "ret_ma = eq_ma.pct_change().fillna(0)\n", + "roll_sharpe = ret_ma.rolling(60).mean() / ret_ma.rolling(60).std() * np.sqrt(252)\n", + "roll_vol = ret_ma.rolling(60).std() * np.sqrt(252)\n", + "\n", + "ax7a.plot(roll_sharpe, color=\"steelblue\", linewidth=1)\n", + "ax7a.axhline(0, color=\"black\", linewidth=0.8, linestyle=\"--\")\n", + "ax7a.axhline(1, color=\"green\", linewidth=0.8, linestyle=\":\", label=\"Sharpe=1 (目标线)\")\n", + "ax7a.fill_between(roll_sharpe.index, roll_sharpe, 0,\n", + " where=(roll_sharpe > 0), alpha=0.2, color=\"green\")\n", + "ax7a.fill_between(roll_sharpe.index, roll_sharpe, 0,\n", + " where=(roll_sharpe < 0), alpha=0.2, color=\"red\")\n", + "ax7a.set_title(\"滚动60日夏普比率 (60-day Rolling Sharpe)\", fontsize=11, fontweight=\"bold\")\n", + "ax7a.set_ylabel(\"夏普比率 Sharpe\")\n", + "ax7a.legend(fontsize=8)\n", + "ax7a.grid(alpha=0.3)\n", + "\n", + "ax7b.plot(roll_vol, color=\"darkorange\", linewidth=1)\n", + "ax7b.fill_between(roll_vol.index, roll_vol, alpha=0.2, color=\"orange\")\n", + "ax7b.set_title(\"滚动60日年化波动率 (60-day Rolling Volatility)\", fontsize=11, fontweight=\"bold\")\n", + "ax7b.set_ylabel(\"年化波动率 Ann. Volatility\")\n", + "ax7b.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"{x:.0%}\"))\n", + "ax7b.grid(alpha=0.3)\n", + "\n", + "plt.suptitle(\n", + " \"量化交易 — 事件驱动回测引擎 Quantitative Trading: Event-Driven Backtest\",\n", + " fontsize=15, fontweight=\"bold\", y=1.003,\n", + ")\n", + "plt.savefig(\"event_driven_backtest.png\", dpi=120, bbox_inches=\"tight\")\n", + "plt.show()\n", + "print(\"\\n[图表] 已保存至 event_driven_backtest.png\")" + ] + }, + { + "cell_type": "markdown", + "id": "ed738eb1", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 9: Compare Event-Driven vs Vectorized\n", + "# 对比事件驱动 vs 向量化回测\n", + "\n", + "# =============================================================================\n", + "#\n", + "# Run a quick vectorized version of the SAME MA strategy on the SAME data\n", + "# to show how results can differ due to:\n", + "# 对相同数据运行相同MA策略的快速向量化版本,展示结果差异的原因:\n", + "#\n", + "# 1. Fill timing 成交时机 — event-driven uses next-bar open;\n", + "# vectorized typically uses same-bar close\n", + "# 2. Cost modeling 成本建模 — event-driven has min commission floor;\n", + "# vectorized applies flat percentage only\n", + "# 3. Integer shares 整数股 — event-driven floors to whole shares;\n", + "# vectorized trades fractional shares\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ab80536", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"\\n{'=' * 60}\")\n", + "print(\" 对比: 事件驱动 vs 向量化回测\")\n", + "print(\" Comparison: Event-Driven vs Vectorized Backtest\")\n", + "print(f\"{'=' * 60}\")\n", + "print(\" 相同策略: 双均线 MA Crossover (20/60)\")\n", + "print(\" 相同数据: 同一模拟价格序列\")\n", + "print()\n", + "\n", + "# Vectorized version (快速向量化版本)\n", + "closes = ohlcv[\"close\"]\n", + "sma20v = closes.rolling(20).mean()\n", + "sma60v = closes.rolling(60).mean()\n", + "sig_vec = (sma20v > sma60v).astype(float).shift(1).fillna(0)\n", + "ret_vec = closes.pct_change().fillna(0)\n", + "strat_ret_gross = sig_vec * ret_vec\n", + "# Simple flat cost: 0.03% commission + 0.01% slippage = 0.04% per side\n", + "position_change = sig_vec.diff().abs().fillna(0)\n", + "cost_vec = position_change * (0.0003 + 0.0001)\n", + "strat_ret_net = strat_ret_gross - cost_vec\n", + "eq_vec = 1_000_000 * (1 + strat_ret_net).cumprod()\n", + "\n", + "# Compare\n", + "m_ed = engine_ma.metrics()\n", + "total_ed = float(m_ed[\"总收益率 Total Return\"].strip(\"%\")) / 100\n", + "sharpe_ed = float(m_ed[\"夏普比率 Sharpe Ratio\"])\n", + "maxdd_ed = float(m_ed[\"最大回撤 Max Drawdown\"].strip(\"%\")) / 100\n", + "\n", + "total_vec = (eq_vec.iloc[-1] / 1_000_000) - 1\n", + "ret_v_s = strat_ret_net\n", + "sharpe_vec = (ret_v_s.mean() / ret_v_s.std()) * np.sqrt(252)\n", + "rolling_mx = eq_vec.cummax()\n", + "maxdd_vec = ((eq_vec - rolling_mx) / rolling_mx).min()\n", + "\n", + "print(f\" {'指标 Metric':<30} {'事件驱动 Event-Driven':>22} {'向量化 Vectorized':>20}\")\n", + "print(f\" {'─'*72}\")\n", + "print(f\" {'总收益率 Total Return':<30} {total_ed:>21.2%} {total_vec:>19.2%}\")\n", + "print(f\" {'夏普比率 Sharpe Ratio':<30} {sharpe_ed:>21.3f} {sharpe_vec:>19.3f}\")\n", + "print(f\" {'最大回撤 Max Drawdown':<30} {maxdd_ed:>21.2%} {maxdd_vec:>19.2%}\")\n", + "print()\n", + "print(\" 差异原因 / Why they differ:\")\n", + "print(\" ① 事件驱动以「下一日开盘价」成交 (Event-driven fills at next open)\")\n", + "print(\" 向量化以「当日收盘价」交易 (Vectorized trades at same-day close)\")\n", + "print(\" ② 事件驱动有最低佣金下限 ¥5 (Event-driven has min commission floor)\")\n", + "print(\" ③ 事件驱动按整数股买卖 (Event-driven uses integer share quantities)\")\n", + "print(\" ④ 资金是随时间变化的 (Capital base grows/shrinks with P&L)\")" + ] + }, + { + "cell_type": "markdown", + "id": "c08fbbf2", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 10: What's Next 下一步\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a0523673", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"\"\"\n", + "{'=' * 72}\n", + " 总结 Summary\n", + "{'=' * 72}\n", + "\n", + " 事件驱动回测引擎的核心组件 / Core Components of Event-Driven Engine:\n", + "\n", + " ┌─────────────────────┬────────────────────────────────────────────┐\n", + " │ 组件 Component │ 职责 Responsibility │\n", + " ├─────────────────────┼────────────────────────────────────────────┤\n", + " │ Event (事件) │ 解耦组件间通信的消息对象 │\n", + " │ │ Message objects that decouple components │\n", + " ├─────────────────────┼────────────────────────────────────────────┤\n", + " │ DataHandler │ 逐根K线流出历史数据(模拟无未来信息) │\n", + " │ 数据处理器 │ Streams bars one-by-one (no future info) │\n", + " ├─────────────────────┼────────────────────────────────────────────┤\n", + " │ Strategy (策略) │ 观察市场 → 生成交易信号 │\n", + " │ │ Observes market → generates trade signals │\n", + " ├─────────────────────┼────────────────────────────────────────────┤\n", + " │ Portfolio (组合) │ 仓位管理 + 现金/持仓/盈亏追踪 │\n", + " │ │ Position sizing + cash/position/P&L track │\n", + " ├─────────────────────┼────────────────────────────────────────────┤\n", + " │ SimulatedBroker │ 模拟券商:滑点+佣金+成交时机 │\n", + " │ 模拟券商 │ Simulates broker: slippage+commission+fill │\n", + " ├─────────────────────┼────────────────────────────────────────────┤\n", + " │ BacktestEngine │ 主事件循环,驱动所有组件 │\n", + " │ 回测引擎 │ Main event loop, orchestrates all parts │\n", + " └─────────────────────┴────────────────────────────────────────────┘\n", + "\n", + " 下一步学习方向 Next Steps:\n", + " ──────────────────────────────────────────────────────────────────\n", + " • 限价单/止损单 Limit & Stop orders in ExecutionHandler\n", + " • 多标的组合 Multi-asset portfolio (stocks + bonds, sector rotation)\n", + " • 因子选股 Alpha factor models (Fama-French, Momentum, Quality)\n", + " • 机器学习信号 ML-based signals (XGBoost, LSTM)\n", + " • 风险管理 Risk management (VaR, CVaR, Kelly position sizing)\n", + " • 实盘对接 Live trading connection (vnpy, Alpaca API)\n", + "{'=' * 72}\n", + "\"\"\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (trading)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/quant_portfolio_optimization_demo.ipynb b/quant_portfolio_optimization_demo.ipynb new file mode 100644 index 0000000..74e421a --- /dev/null +++ b/quant_portfolio_optimization_demo.ipynb @@ -0,0 +1,1102 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "26fdd646", + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + "量化交易演示系列 第5篇:组合优化\n", + "Quantitative Trading Demo Series - Part 5: Portfolio Optimization\n", + "\n", + "涵盖内容 / Topics Covered:\n", + " §0 合成股票池与因子模型 (Synthetic Universe & Factor Model)\n", + " §1 协方差矩阵估计 (Covariance Matrix Estimation)\n", + " §2 马科维兹均值-方差优化 (Markowitz Mean-Variance Optimization)\n", + " §3 有效前沿 (Efficient Frontier)\n", + " §4 特殊组合:等权/最小方差/最大夏普 (Special Portfolios: EW / MinVar / MaxSharpe)\n", + " §5 风险平价 (Risk Parity)\n", + " §6 Black-Litterman 模型 (Black-Litterman Model)\n", + " §7 带约束的组合优化 (Constrained Portfolio Optimization)\n", + " §8 滚动回测:五策略对比 (Rolling Backtest: 5-Strategy Comparison)\n", + " §9 可视化 (9-Panel Visualization)\n", + "\n", + "依赖库 / Dependencies:\n", + " numpy, pandas, scipy, matplotlib, sklearn\n", + "\n", + "作者 / Author: Quant Demo Series\n", + "\"\"\"\n", + "\n", + "# ── 无界面绘图模式,必须在 import pyplot 之前设置 ──\n", + "# Headless plotting mode — MUST be set before importing pyplot\n", + "import matplotlib\n", + "matplotlib.use('Agg')\n", + "\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.gridspec as gridspec\n", + "from matplotlib.ticker import FuncFormatter\n", + "from scipy.optimize import minimize\n", + "\n", + "# ── 随机种子(确保结果可复现)/ Random seed for reproducibility ──\n", + "np.random.seed(42)\n", + "\n", + "# ── 中文字体配置 / Chinese font configuration ──\n", + "plt.rcParams['font.sans-serif'] = [\n", + " 'WenQuanYi Zen Hei', 'Arial Unicode MS', 'SimHei', 'DejaVu Sans'\n", + "]\n", + "plt.rcParams['axes.unicode_minus'] = False\n", + "\n", + "print(\"=\" * 70)\n", + "print(\" 量化交易 组合优化演示\")\n", + "print(\" Quantitative Trading: Portfolio Optimization Demo\")\n", + "print(\"=\" * 70)" + ] + }, + { + "cell_type": "markdown", + "id": "100ee590", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════\n", + "# §0 合成股票池与行业因子模型\n", + "# Synthetic Universe & Sector Factor Model\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════\n", + "#\n", + "# 使用 Barra 风格的因子模型生成收益率:\n", + "# Returns are generated using a Barra-style factor model:\n", + "#\n", + "# r_{i,t} = β_{mkt,i} × r_{mkt,t} ← 市场因子 (Market factor)\n", + "# + Σ_k β_{sec,i,k} × f_{k,t} ← 行业因子 (Sector factors)\n", + "# + α_i ← 个股 Alpha (Stock-specific alpha)\n", + "# + ε_{i,t} ← 特质噪音 (Idiosyncratic noise)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94603f7d", + "metadata": {}, + "outputs": [], + "source": [ + "N_STOCKS = 20 # 股票数量 (Number of stocks)\n", + "N_YEARS = 3 # 历史年数 (History in years)\n", + "FREQ = 252 # 年化因子 / 每年交易日数 (Annualization factor)\n", + "RF = 0.02 # 无风险利率 (Risk-free rate), 年化\n", + "\n", + "START_DATE = \"2021-01-04\"\n", + "dates = pd.bdate_range(START_DATE, periods=N_YEARS * FREQ) # 工作日序列\n", + "N_DAYS = len(dates)\n", + "\n", + "# ── 行业分组 / Sector assignments ──\n", + "SECTORS = {\n", + " \"科技 Tech\": list(range(0, 5)), # S00–S04\n", + " \"金融 Finance\": list(range(5, 9)), # S05–S08\n", + " \"消费 Consumer\": list(range(9, 13)), # S09–S12\n", + " \"工业 Industrial\": list(range(13, 17)), # S13–S16\n", + " \"医疗 Healthcare\": list(range(17, 20)), # S17–S19\n", + "}\n", + "SECTOR_NAMES = list(SECTORS.keys())\n", + "SECTOR_SHORT = [\"科技\", \"金融\", \"消费\", \"工业\", \"医疗\"]\n", + "N_SECTORS = len(SECTORS)\n", + "stock_names = [f\"S{i:02d}\" for i in range(N_STOCKS)]\n", + "\n", + "# ── 行业公共因子收益率 / Sector factor daily returns ──\n", + "sector_ann_ret = np.array([0.16, 0.10, 0.12, 0.08, 0.13]) # 各行业年化期望收益\n", + "sector_ann_vol = np.array([0.25, 0.18, 0.20, 0.15, 0.22]) # 各行业年化波动率\n", + "\n", + "sector_factor_rets = np.zeros((N_DAYS, N_SECTORS))\n", + "for k in range(N_SECTORS):\n", + " mu_d = sector_ann_ret[k] / FREQ\n", + " sig_d = sector_ann_vol[k] / np.sqrt(FREQ)\n", + " sector_factor_rets[:, k] = np.random.normal(mu_d, sig_d, N_DAYS)\n", + "\n", + "# ── 市场公共因子 / Market common factor ──\n", + "mkt_daily = np.random.normal(0.10 / FREQ, 0.18 / np.sqrt(FREQ), N_DAYS)\n", + "\n", + "# ── 各股的因子载荷(Beta 暴露)/ Factor loadings (Beta exposures) ──\n", + "betas_mkt = np.random.uniform(0.6, 1.4, N_STOCKS) # 市场 Beta\n", + "betas_sec = np.zeros((N_STOCKS, N_SECTORS)) # 行业 Beta\n", + "\n", + "for k, (sname, sidx) in enumerate(SECTORS.items()):\n", + " for i in sidx:\n", + " betas_sec[i, k] = np.random.uniform(0.5, 1.0) # 本行业:强载荷\n", + " for k2 in range(N_SECTORS):\n", + " if k2 != k:\n", + " betas_sec[i, k2] = np.random.uniform(0.0, 0.15) # 其他行业:弱载荷\n", + "\n", + "# ── 个股特质波动率 / Idiosyncratic volatility ──\n", + "idio_vols = np.random.uniform(0.12, 0.30, N_STOCKS) / np.sqrt(FREQ)\n", + "idio_rets = np.random.normal(0, idio_vols, (N_DAYS, N_STOCKS))\n", + "\n", + "# ── 个股特质 Alpha / Stock-specific alpha ──\n", + "# 科技股注入正 Alpha,让最大夏普组合有明显的倾向性\n", + "idio_alpha = np.zeros(N_STOCKS)\n", + "idio_alpha[:5] = np.random.uniform(0.04, 0.09, 5) / FREQ # 科技股:正 Alpha\n", + "idio_alpha[5:9] = np.random.uniform(-0.02, 0.02, 4) / FREQ # 金融股:中性\n", + "\n", + "# ── 合成日收益率矩阵 / Composite daily return matrix ──\n", + "daily_ret_mat = np.zeros((N_DAYS, N_STOCKS))\n", + "for i in range(N_STOCKS):\n", + " daily_ret_mat[:, i] = (\n", + " betas_mkt[i] * mkt_daily # 市场贡献\n", + " + betas_sec[i] @ sector_factor_rets.T # 行业贡献(5 行业因子)\n", + " + idio_alpha[i] # 个股 Alpha\n", + " + idio_rets[:, i] # 特质噪音\n", + " )\n", + "\n", + "ret_df = pd.DataFrame(daily_ret_mat, index=dates, columns=stock_names)\n", + "prices_df = (1 + ret_df).cumprod() * 100.0 # 从 100 元开始的价格序列\n", + "\n", + "# ── 市值权重(Black-Litterman 需要作为先验市场组合)──\n", + "# Market-cap weights as Black-Litterman equilibrium prior\n", + "mktcap_weights = pd.Series(\n", + " np.random.dirichlet(np.ones(N_STOCKS) * 2.0),\n", + " index=stock_names\n", + ")\n", + "\n", + "ann_rets = (1 + ret_df).prod() ** (FREQ / N_DAYS) - 1\n", + "print(f\"\\n[§0] 股票池生成完成 / Universe Generated\")\n", + "print(f\" 股票数量 (Stocks): {N_STOCKS} 只\")\n", + "print(f\" 交易日数 (Days): {N_DAYS} 天 {dates[0].date()} ~ {dates[-1].date()}\")\n", + "print(f\" 行业数量 (Sectors): {N_SECTORS} 个\")\n", + "print(f\" 个股年化收益区间: {ann_rets.min():.1%} ~ {ann_rets.max():.1%}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7edbe870", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════\n", + "# §1 协方差矩阵估计:样本 vs Ledoit-Wolf 收缩\n", + "# Covariance Matrix Estimation: Sample vs Ledoit-Wolf Shrinkage\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════\n", + "#\n", + "# 协方差矩阵 (Covariance Matrix) Σ 是组合优化的核心输入。\n", + "# 样本协方差矩阵 (Sample Covariance Matrix) 存在两个问题:\n", + "# 1. 估计误差大(小样本情形,T/N 较小时)/ Large estimation error when T/N is small\n", + "# 2. 条件数 (Condition Number) 高,矩阵\"病态\",优化对估计误差非常敏感\n", + "#\n", + "# Ledoit-Wolf 收缩 (Ledoit-Wolf Shrinkage):\n", + "# Σ_shrunk = (1-α) × Σ_sample + α × F\n", + "# 其中 F 是结构化目标矩阵(如对角矩阵),α 是最优收缩系数(数据驱动自动选择)\n", + "# F is a structured target matrix; α is the optimal shrinkage coefficient.\n", + "\n", + "# 样本协方差(年化) / Sample covariance (annualized)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8d795d7", + "metadata": {}, + "outputs": [], + "source": [ + "cov_sample = ret_df.cov() * FREQ\n", + "mu_sample = ret_df.mean() * FREQ # 年化预期收益向量 (Annualized expected return vector)\n", + "\n", + "# Ledoit-Wolf 收缩估计 / Ledoit-Wolf shrinkage estimation\n", + "try:\n", + " from sklearn.covariance import LedoitWolf\n", + " lw = LedoitWolf()\n", + " lw.fit(ret_df.values)\n", + " cov_shrink = pd.DataFrame(\n", + " lw.covariance_ * FREQ,\n", + " index=stock_names, columns=stock_names\n", + " )\n", + " shrink_alpha = lw.shrinkage_ # 最优收缩系数 α\n", + " USE_SHRINK = True\n", + "except ImportError:\n", + " cov_shrink = cov_sample.copy()\n", + " shrink_alpha = 0.0\n", + " USE_SHRINK = False\n", + "\n", + "print(f\"\\n[§1] 协方差估计 / Covariance Estimation\")\n", + "print(f\" 样本协方差条件数 (Sample Cov Cond#): {np.linalg.cond(cov_sample.values):.1f}\")\n", + "if USE_SHRINK:\n", + " print(f\" Ledoit-Wolf 收缩系数 (Shrinkage α): {shrink_alpha:.4f}\")\n", + " print(f\" 收缩后条件数 (Shrunk Cov Cond#): {np.linalg.cond(cov_shrink.values):.1f}\")\n", + " print(f\" → 条件数降低意味着优化更稳健 / Lower cond# = more robust optimization\")\n", + "\n", + "# 正式使用的协方差矩阵(收缩版更稳健)/ Use shrunk covariance for optimization\n", + "cov_opt = cov_shrink if USE_SHRINK else cov_sample\n", + "mu_opt = mu_sample.copy()" + ] + }, + { + "cell_type": "markdown", + "id": "aab1c601", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════\n", + "# §2 核心优化工具函数\n", + "# Core Optimization Utility Functions\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0f68b49", + "metadata": {}, + "outputs": [], + "source": [ + "def portfolio_stats(weights, mu, cov, rf=RF):\n", + " \"\"\"\n", + " 计算组合的三个核心绩效指标(年化)\n", + " Compute 3 key portfolio statistics (annualized).\n", + "\n", + " 参数 / Args:\n", + " weights : np.ndarray, 权重向量 (weight vector), shape (N,)\n", + " mu : pd.Series, 年化预期收益向量 (annualized expected returns)\n", + " cov : pd.DataFrame, 年化协方差矩阵 (annualized covariance matrix)\n", + " rf : float, 无风险利率 (risk-free rate)\n", + "\n", + " 返回 / Returns:\n", + " ret : 组合年化收益率 (portfolio annualized return)\n", + " vol : 组合年化波动率 (portfolio annualized volatility)\n", + " sharpe : 夏普比率 (Sharpe ratio) = (ret - rf) / vol\n", + " \"\"\"\n", + " w = np.asarray(weights)\n", + " ret = float(w @ mu) # w'μ\n", + " var = float(w @ cov.values @ w) # w'Σw(组合方差)\n", + " vol = np.sqrt(max(var, 1e-12)) # 组合波动率(年化)\n", + " sharpe = (ret - rf) / vol\n", + " return ret, vol, sharpe\n", + "\n", + "\n", + "def _base_optimize(objective, n, bounds, extra_constraints=None):\n", + " \"\"\"\n", + " 通用 SLSQP 优化框架(内部使用)\n", + " Generic SLSQP optimization scaffold (internal use).\n", + "\n", + " SLSQP = Sequential Least Squares Programming / 序列二次规划法\n", + " \"\"\"\n", + " w0 = np.ones(n) / n # 初始猜测:等权 (Initial guess: equal weight)\n", + " constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1.0}] # 全投资约束\n", + " if extra_constraints:\n", + " constraints.extend(extra_constraints)\n", + " return minimize(\n", + " objective, w0,\n", + " method='SLSQP',\n", + " bounds=bounds,\n", + " constraints=constraints,\n", + " options={'ftol': 1e-12, 'maxiter': 1500}\n", + " )\n", + "\n", + "\n", + "def min_variance(mu, cov, allow_short=False):\n", + " \"\"\"\n", + " 最小方差组合 (Minimum Variance Portfolio, MinVar)\n", + "\n", + " 这是组合优化中最稳健的组合,因为它完全不依赖对预期收益的估计。\n", + " Most robust portfolio: it does NOT require any return estimates.\n", + "\n", + " 问题形式 / Optimization problem:\n", + " minimize w'Σw\n", + " subject to Σw_i = 1 (全投资约束 / full investment)\n", + " w_i ≥ 0 (多头约束 / long-only, when allow_short=False)\n", + " \"\"\"\n", + " n = len(mu)\n", + " bnd = (-0.3, 1.0) if allow_short else (0.0, 1.0)\n", + " bounds = [bnd] * n\n", + " result = _base_optimize(lambda w: w @ cov.values @ w, n, bounds)\n", + " return pd.Series(result.x, index=cov.index)\n", + "\n", + "\n", + "def max_sharpe(mu, cov, rf=RF, allow_short=False):\n", + " \"\"\"\n", + " 最大夏普比率组合 / 切线组合 (Maximum Sharpe / Tangency Portfolio)\n", + "\n", + " 在均值-方差空间中,从无风险资产出发到有效前沿的切线点。\n", + " Tangency point from the risk-free asset to the efficient frontier (Capital Market Line).\n", + "\n", + " 技巧 / Trick:\n", + " max Sharpe ≡ min -Sharpe (把最大化转为最小化 / flip to minimization)\n", + " \"\"\"\n", + " n = len(mu)\n", + " bnd = (-0.3, 1.0) if allow_short else (0.0, 1.0)\n", + " bounds = [bnd] * n\n", + "\n", + " def neg_sharpe(w):\n", + " r, v, sr = portfolio_stats(w, mu, cov, rf)\n", + " return -sr\n", + "\n", + " result = _base_optimize(neg_sharpe, n, bounds)\n", + " return pd.Series(result.x, index=cov.index)\n", + "\n", + "\n", + "def efficient_portfolio(target_return, mu, cov):\n", + " \"\"\"\n", + " 目标收益下的最小方差组合(有效前沿上的一点)\n", + " Minimum-variance portfolio for a given target return (a point on efficient frontier).\n", + "\n", + " 问题形式 / Optimization problem:\n", + " minimize w'Σw\n", + " subject to w'μ = target_return (收益约束 / return constraint)\n", + " Σw_i = 1\n", + " w_i ≥ 0\n", + " \"\"\"\n", + " n = len(mu)\n", + " bnd = (0.0, 1.0)\n", + " result = _base_optimize(\n", + " lambda w: w @ cov.values @ w, n,\n", + " bounds=[bnd] * n,\n", + " extra_constraints=[\n", + " {'type': 'eq', 'fun': lambda w: float(w @ mu.values) - target_return}\n", + " ]\n", + " )\n", + " return pd.Series(result.x, index=cov.index) if result.success else None" + ] + }, + { + "cell_type": "markdown", + "id": "c5dc678e", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════\n", + "# §3 有效前沿\n", + "# Efficient Frontier\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════\n", + "#\n", + "# 有效前沿 (Efficient Frontier) 是所有\"有效组合\"的集合:\n", + "# → 在相同风险(波动率)下,收益最高\n", + "# → 在相同收益下,风险最低\n", + "#\n", + "# 有效前沿由两点确定:\n", + "# 左端点:最小方差组合 (Global Minimum Variance Portfolio, GMV)\n", + "# 右端点:预期收益最高的单只股票(极端集中)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4f0caa1", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"\\n[§3] 计算有效前沿 / Computing Efficient Frontier...\")\n", + "\n", + "# ── 计算关键节点组合 / Compute key special portfolios ──\n", + "w_minvar = min_variance(mu_opt, cov_opt)\n", + "w_maxsharpe = max_sharpe(mu_opt, cov_opt, rf=RF)\n", + "w_eq = pd.Series(np.ones(N_STOCKS) / N_STOCKS, index=stock_names)\n", + "\n", + "ret_min, vol_min, sr_min = portfolio_stats(w_minvar, mu_opt, cov_opt)\n", + "ret_msr, vol_msr, sr_msr = portfolio_stats(w_maxsharpe, mu_opt, cov_opt)\n", + "ret_eq, vol_eq, sr_eq = portfolio_stats(w_eq, mu_opt, cov_opt)\n", + "\n", + "# ── 扫描有效前沿 / Trace the efficient frontier ──\n", + "n_pts = 60\n", + "ret_lo = ret_min * 0.99\n", + "ret_hi = mu_opt.max() * 0.88\n", + "frontier_rets = []\n", + "frontier_vols = []\n", + "\n", + "for tgt in np.linspace(ret_lo, ret_hi, n_pts):\n", + " w_eff = efficient_portfolio(tgt, mu_opt, cov_opt)\n", + " if w_eff is not None:\n", + " r, v, _ = portfolio_stats(w_eff, mu_opt, cov_opt)\n", + " frontier_rets.append(r)\n", + " frontier_vols.append(v)\n", + "\n", + "# ── Monte Carlo 随机组合云(背景参照)/ Random portfolio cloud (background reference) ──\n", + "N_RANDOM = 3000\n", + "rand_rets = []\n", + "rand_vols = []\n", + "rand_sharpes = []\n", + "\n", + "for _ in range(N_RANDOM):\n", + " w_r = np.random.dirichlet(np.ones(N_STOCKS)) # 满足 w≥0, Σw=1 的随机权重\n", + " r, v, sr = portfolio_stats(w_r, mu_opt, cov_opt)\n", + " rand_rets.append(r)\n", + " rand_vols.append(v)\n", + " rand_sharpes.append(sr)\n", + "\n", + "print(f\" 有效前沿点数 (Frontier points): {len(frontier_rets)}\")\n", + "print(f\" 最小方差组合 (MinVar): 收益={ret_min:.1%}, 波动={vol_min:.1%}, 夏普={sr_min:.2f}\")\n", + "print(f\" 最大夏普组合 (MaxSharpe): 收益={ret_msr:.1%}, 波动={vol_msr:.1%}, 夏普={sr_msr:.2f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "69d5e842", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════\n", + "# §4 风险平价\n", + "# Risk Parity\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════\n", + "#\n", + "# 核心思想:每只资产对组合总风险的贡献相等\n", + "# Core idea: each asset contributes equally to total portfolio risk.\n", + "#\n", + "# 风险贡献 (Risk Contribution, RC) 的定义:\n", + "#\n", + "# RC_i = w_i × ∂σ_p/∂w_i = w_i × (Σw)_i / σ_p\n", + "# ↑ 边际风险贡献 (Marginal Risk Contribution, MRC)\n", + "#\n", + "# 等风险贡献条件 (Equal Risk Contribution):\n", + "# RC_i = σ_p / N ←→ w_i × (Σw)_i = w_j × (Σw)_j for all i, j\n", + "#\n", + "# 优化形式(最小化各股风险贡献比例与目标 1/N 的偏差平方和):\n", + "# minimize Σ_i (RC_i/σ_p − 1/N)²" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a033b66c", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"\\n[§4] 计算风险平价组合 / Risk Parity Portfolio...\")\n", + "\n", + "\n", + "def risk_contributions(weights, cov):\n", + " \"\"\"\n", + " 计算每只资产的绝对风险贡献和风险贡献占比\n", + " Compute absolute risk contribution and risk contribution fraction for each asset.\n", + "\n", + " Returns:\n", + " rc : 绝对风险贡献向量 (Absolute Risk Contribution vector), shape (N,)\n", + " sigma_p : 组合总波动率 (Total portfolio volatility)\n", + " \"\"\"\n", + " w = np.asarray(weights)\n", + " sigma_p = np.sqrt(float(w @ cov.values @ w)) # 组合波动率 (Portfolio vol)\n", + " mrc = cov.values @ w / sigma_p # 边际风险贡献 (MRC = ∂σ/∂w)\n", + " rc = w * mrc # 绝对风险贡献 (RC = w × MRC)\n", + " return rc, sigma_p\n", + "\n", + "\n", + "def risk_parity(cov):\n", + " \"\"\"\n", + " 等风险贡献组合 (Equal Risk Contribution Portfolio)\n", + "\n", + " 约束:w_i ≥ 0 (严格多头,做空会有负的风险贡献,没有意义)\n", + " Strictly long-only: short positions would give negative RC (meaningless).\n", + " \"\"\"\n", + " N = cov.shape[0]\n", + " tgt = 1.0 / N # 目标风险贡献占比 = 1/N\n", + "\n", + " def objective(w):\n", + " rc, sigma_p = risk_contributions(w, cov)\n", + " rc_pct = rc / sigma_p # 风险贡献占比 (RC fraction)\n", + " return np.sum((rc_pct - tgt) ** 2)\n", + "\n", + " result = _base_optimize(\n", + " objective, N,\n", + " bounds=[(1e-6, 1.0)] * N # 严格正数(不允许做空)\n", + " )\n", + " return pd.Series(result.x, index=cov.index)\n", + "\n", + "\n", + "w_rp = risk_parity(cov_opt)\n", + "rc_rp, sigma_rp = risk_contributions(w_rp, cov_opt)\n", + "ret_rp, vol_rp, sr_rp = portfolio_stats(w_rp, mu_opt, cov_opt)\n", + "\n", + "rc_pct_rp = rc_rp / sigma_rp\n", + "max_rc_dev = np.max(np.abs(rc_pct_rp - 1.0 / N_STOCKS)) # 与目标的最大偏差\n", + "\n", + "print(f\" 风险平价组合: 收益={ret_rp:.1%}, 波动={vol_rp:.1%}, 夏普={sr_rp:.2f}\")\n", + "print(f\" 风险贡献最大偏差 (Max RC deviation from 1/N): {max_rc_dev:.5f}\")\n", + "print(f\" → 接近 0 表示各股风险贡献几乎相等 / ≈0 means near-perfect equal RC\")" + ] + }, + { + "cell_type": "markdown", + "id": "c44140a6", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════\n", + "# §5 Black-Litterman 模型\n", + "# Black-Litterman Model\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════\n", + "#\n", + "# 马科维兹优化的两大痛点 / Two pain points of Markowitz optimization:\n", + "# ① 预期收益 μ 难以估计,哪怕小幅误差也会导致权重大幅波动\n", + "# Expected returns are noisy; small errors cause wild weight swings.\n", + "# ② 结果过度集中(\"角点解\"),实际不可用\n", + "# Results are over-concentrated \"corner solutions\".\n", + "#\n", + "# Black-Litterman (1990) 的贝叶斯框架 / Bayesian framework:\n", + "# ① 先验 (Prior): 从市场均衡推导隐含收益(CAPM 反向优化)\n", + "# Market-implied returns from reverse-optimizing CAPM.\n", + "# ② 观点 (Views): 投资者对某些股票/组合的主观预期\n", + "# Investor's subjective views on specific stocks/portfolios.\n", + "# ③ 后验 (Posterior): 贝叶斯更新,先验与观点的加权混合\n", + "# Bayesian posterior blending prior and views.\n", + "#\n", + "# 后验均值公式 / Posterior mean formula (He & Litterman, 1999):\n", + "#\n", + "# μ_BL = [(τΣ)⁻¹ + P'Ω⁻¹P]⁻¹ × [(τΣ)⁻¹Π + P'Ω⁻¹Q]\n", + "# ───────────────────── ──────────────────────\n", + "# 精度矩阵之和 加权均值向量\n", + "# Sum of precision Weighted mean vector\n", + "#\n", + "# Π = δ × Σ × w_mkt ← 市场均衡隐含超额收益 (Market equilibrium implied returns)\n", + "# δ = 风险厌恶系数 (Risk aversion coefficient)\n", + "# τ = 先验收缩参数 (Prior scaling factor), 通常 0.025~0.10\n", + "# P = 观点矩阵 K×N (View matrix, K views, N assets)\n", + "# Q = 观点收益向量 K×1 (View expected excess returns)\n", + "# Ω = 观点不确定性矩阵 K×K (View uncertainty / confidence matrix)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b41e55ba", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"\\n[§5] Black-Litterman 模型 / Black-Litterman Model...\")\n", + "\n", + "DELTA = 2.5 # 风险厌恶系数 (Risk aversion), 典型值 2.0~3.0\n", + "TAU = 0.05 # 先验收缩参数 (Prior scaling), 典型值 0.025~0.10\n", + "\n", + "# ─── Step 1: 计算市场均衡隐含超额收益 ───\n", + "# Market equilibrium implied excess returns (Reverse Optimization of CAPM)\n", + "# Π = δ × Σ × w_mkt\n", + "w_mkt = mktcap_weights.values\n", + "Pi = DELTA * cov_opt.values @ w_mkt\n", + "Pi_s = pd.Series(Pi, index=stock_names)\n", + "\n", + "# ─── Step 2: 设定投资者观点 (Investor Views) ───\n", + "#\n", + "# 观点1 (View 1): \"S01 的年化超额收益率将达到 +5%\"(单资产绝对观点)\n", + "# \"S01 will earn +5% excess return per year\" (absolute view)\n", + "#\n", + "# 观点2 (View 2): \"S00 将比 S04 多赚 3%\"(两资产相对观点)\n", + "# \"S00 will outperform S04 by 3%\" (relative view)\n", + "\n", + "K = 2 # 观点数量 (Number of views)\n", + "\n", + "# P 矩阵:K × N;P[k, i] = 资产 i 在观点 k 中的权重(正=多头,负=空头)\n", + "P = np.zeros((K, N_STOCKS))\n", + "P[0, 1] = 1.0 # 观点1:仅 S01\n", + "P[1, 0] = 1.0 # 观点2:S00 多头\n", + "P[1, 4] = -1.0 # S04 空头(相对观点)\n", + "\n", + "# Q 向量:观点的预期超额收益 (View expected excess returns)\n", + "Q = np.array([0.05, 0.03])\n", + "\n", + "# Ω 矩阵:观点的不确定性(He & Litterman 建议的设置方式)\n", + "# Ω = τ × diag(P Σ P'),对角元素越大表示该观点越不确定\n", + "Omega = np.diag(TAU * np.diag(P @ cov_opt.values @ P.T))\n", + "Omega_inv = np.linalg.inv(Omega)\n", + "\n", + "# ─── Step 3: 贝叶斯更新,计算后验预期超额收益 ───\n", + "# Bayesian posterior expected excess returns\n", + "tau_cov_inv = np.linalg.inv(TAU * cov_opt.values)\n", + "A = tau_cov_inv + P.T @ Omega_inv @ P # 精度矩阵之和\n", + "b = tau_cov_inv @ Pi + P.T @ Omega_inv @ Q\n", + "\n", + "mu_bl = np.linalg.solve(A, b) + RF # 后验 = 超额 + 无风险利率\n", + "mu_bl_s = pd.Series(mu_bl, index=stock_names)\n", + "\n", + "# ─── Step 4: 用后验收益做最大夏普优化 ───\n", + "w_bl = max_sharpe(mu_bl_s, cov_opt, rf=RF)\n", + "ret_bl, vol_bl, sr_bl = portfolio_stats(w_bl, mu_bl_s, cov_opt)\n", + "\n", + "print(f\" 市场均衡隐含收益范围: {Pi_s.min():.1%} ~ {Pi_s.max():.1%}\")\n", + "print(f\" BL 后验收益范围: {mu_bl_s.min():.1%} ~ {mu_bl_s.max():.1%}\")\n", + "print(f\" BL 组合: 收益={ret_bl:.1%}, 波动={vol_bl:.1%}, 夏普={sr_bl:.2f}\")\n", + "print(f\" 观点受益股(S01, S00)权重 BL: {w_bl['S01']:.1%}, {w_bl['S00']:.1%}\")\n", + "print(f\" 观点受益股(S01, S00)权重 等权: {w_eq['S01']:.1%}, {w_eq['S00']:.1%}\")" + ] + }, + { + "cell_type": "markdown", + "id": "b76492d7", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════\n", + "# §6 带约束的组合优化\n", + "# Constrained Portfolio Optimization\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════\n", + "#\n", + "# 实际组合管理中必须满足多重约束 / Real portfolios need multiple constraints:\n", + "# ① 单只股票权重上限(集中度管理)/ Max individual weight (concentration limit)\n", + "# ② 行业权重区间(风格/行业暴露管理)/ Sector weight bounds (style control)\n", + "# ③ 换手率约束(控制交易成本)/ Turnover constraint (transaction cost control)\n", + "#\n", + "# 换手率 (Turnover) 的定义:\n", + "# Turnover = Σ_i |w_new,i - w_old,i| / 2 (双边换手率,除以 2 避免重复计算)\n", + "# 或简化为 L1 距离:Σ_i |w_new,i - w_old,i|" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5b2f82e", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"\\n[§6] 带约束的最大夏普组合 / Constrained Max-Sharpe Portfolio...\")\n", + "\n", + "MAX_STOCK_WT = 0.15 # 单只股票最大 15% / Max 15% per stock\n", + "MAX_SECTOR_WT = 0.35 # 单行业最大 35% / Max 35% per sector\n", + "MIN_SECTOR_WT = 0.10 # 单行业最小 10% / Min 10% per sector\n", + "MAX_TURNOVER = 0.50 # 最大换手率 50% / Max 50% one-way turnover\n", + "\n", + "w_prev = np.ones(N_STOCKS) / N_STOCKS # 上期持仓(假设为等权)\n", + "\n", + "n = N_STOCKS\n", + "w0 = np.ones(n) / n\n", + "\n", + "extra_cons = []\n", + "\n", + "# 约束1: 行业权重上下限\n", + "for sname, sidx in SECTORS.items():\n", + " def make_lb(idx): return lambda w: np.sum(w[idx]) - MIN_SECTOR_WT\n", + " def make_ub(idx): return lambda w: MAX_SECTOR_WT - np.sum(w[idx])\n", + " extra_cons.append({'type': 'ineq', 'fun': make_lb(sidx)}) # ≥ 10%\n", + " extra_cons.append({'type': 'ineq', 'fun': make_ub(sidx)}) # ≤ 35%\n", + "\n", + "# 约束2: 换手率约束(L1 距离 ≤ 50%)\n", + "extra_cons.append({'type': 'ineq',\n", + " 'fun': lambda w: MAX_TURNOVER - np.sum(np.abs(w - w_prev))})\n", + "\n", + "result_c = _base_optimize(\n", + " lambda w: -portfolio_stats(w, mu_opt, cov_opt, RF)[2], # 最小化负夏普\n", + " n,\n", + " bounds=[(0.0, MAX_STOCK_WT)] * n,\n", + " extra_constraints=extra_cons\n", + ")\n", + "w_constrained = pd.Series(result_c.x, index=stock_names)\n", + "ret_c, vol_c, sr_c = portfolio_stats(w_constrained, mu_opt, cov_opt)\n", + "\n", + "print(f\" 带约束最大夏普: 收益={ret_c:.1%}, 波动={vol_c:.1%}, 夏普={sr_c:.2f}\")\n", + "print(f\" 最大单股权重: {w_constrained.max():.1%} (约束 ≤{MAX_STOCK_WT:.0%})\")\n", + "for sname, sidx in SECTORS.items():\n", + " sw = w_constrained.iloc[sidx].sum()\n", + " ok = \"✓\" if MIN_SECTOR_WT <= sw <= MAX_SECTOR_WT else \"✗\"\n", + " print(f\" {ok} {sname}: {sw:.1%}\")" + ] + }, + { + "cell_type": "markdown", + "id": "d91f9e2a", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════\n", + "# §7 滚动回测:五策略对比\n", + "# Rolling Backtest: 5-Strategy Comparison\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════\n", + "#\n", + "# 流程 / Workflow:\n", + "# 每个调仓日 T(每月一次):\n", + "# ① 用过去 LOOKBACK 天估计 μ 和 Σ\n", + "# ② 用当前估计值重新计算各策略权重\n", + "# ③ 用新权重持有到下次调仓日,每日记录组合收益\n", + "#\n", + "# 五种策略 / 5 Strategies:\n", + "# EW : 等权(不需要优化)\n", + "# MinVar : 最小方差(只需 Σ,不需要 μ)\n", + "# MaxSharpe : 最大夏普(需要 μ 和 Σ)\n", + "# RiskParity : 风险平价(只需 Σ)\n", + "# BL : Black-Litterman(贝叶斯混合)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b74dc367", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"\\n[§7] 滚动回测 / Rolling Backtest...\")\n", + "\n", + "LOOKBACK = 126 # 协方差回看期(约 6 个月)/ 6-month estimation window\n", + "REBAL_FREQ = 21 # 调仓频率(约 1 个月)/ Monthly rebalancing\n", + "\n", + "STRATEGY_NAMES = [\n", + " \"等权 EW\",\n", + " \"最小方差 MinVar\",\n", + " \"最大夏普 MaxSharpe\",\n", + " \"风险平价 RP\",\n", + " \"Black-Litterman BL\",\n", + "]\n", + "\n", + "# 初始化:回看期内用 0 填充(尚未开始实际持仓)\n", + "daily_pnl = {name: [0.0] * LOOKBACK for name in STRATEGY_NAMES}\n", + "\n", + "# 当前持仓权重(初始为等权)\n", + "cur_w = {name: np.ones(N_STOCKS) / N_STOCKS for name in STRATEGY_NAMES}\n", + "\n", + "rebal_idx = list(range(LOOKBACK, N_DAYS, REBAL_FREQ))\n", + "\n", + "for t_pos, t in enumerate(rebal_idx):\n", + " # 取过去 LOOKBACK 天的历史收益率\n", + " hist = ret_df.iloc[t - LOOKBACK: t]\n", + "\n", + " # 估计滚动协方差矩阵(加微小正则化保证正定性)\n", + " # Rolling covariance (add tiny ridge term to ensure positive definiteness)\n", + " cov_r_arr = hist.cov().values * FREQ + 1e-7 * np.eye(N_STOCKS)\n", + " mu_r = hist.mean() * FREQ\n", + " cov_r = pd.DataFrame(cov_r_arr, index=stock_names, columns=stock_names)\n", + "\n", + " try:\n", + " new_w = {}\n", + " new_w[\"等权 EW\"] = np.ones(N_STOCKS) / N_STOCKS\n", + " new_w[\"最小方差 MinVar\"] = min_variance(mu_r, cov_r).values\n", + " new_w[\"最大夏普 MaxSharpe\"] = max_sharpe(mu_r, cov_r, rf=RF).values\n", + " new_w[\"风险平价 RP\"] = risk_parity(cov_r).values\n", + "\n", + " # Black-Litterman 滚动版本:使用滚动先验 + 固定观点\n", + " Pi_r = DELTA * cov_r_arr @ mktcap_weights.values\n", + " try:\n", + " tci = np.linalg.inv(TAU * cov_r_arr)\n", + " A_r = tci + P.T @ Omega_inv @ P\n", + " b_r = tci @ Pi_r + P.T @ Omega_inv @ Q\n", + " mu_bl_r = np.linalg.solve(A_r, b_r) + RF\n", + " except np.linalg.LinAlgError:\n", + " mu_bl_r = mu_r.values\n", + " new_w[\"Black-Litterman BL\"] = max_sharpe(\n", + " pd.Series(mu_bl_r, index=stock_names), cov_r, rf=RF\n", + " ).values\n", + "\n", + " cur_w = new_w\n", + "\n", + " except Exception:\n", + " pass # 优化失败时保持上期权重 / Keep previous weights if optimization fails\n", + "\n", + " # 计算持有期内每日组合收益\n", + " next_t = rebal_idx[t_pos + 1] if t_pos + 1 < len(rebal_idx) else N_DAYS\n", + " for name in STRATEGY_NAMES:\n", + " w = cur_w[name]\n", + " for d in range(t, next_t):\n", + " daily_pnl[name].append(float(w @ ret_df.iloc[d].values))\n", + "\n", + "# 截齐到 N_DAYS(防止因最后一段超出)\n", + "for name in STRATEGY_NAMES:\n", + " daily_pnl[name] = daily_pnl[name][:N_DAYS]\n", + "\n", + "# ── 计算净值曲线 (NAV Curves) ──\n", + "nav_df = pd.DataFrame(\n", + " {name: (1 + pd.Series(daily_pnl[name], index=dates)).cumprod()\n", + " for name in STRATEGY_NAMES}\n", + ")\n", + "\n", + "# ── 计算绩效指标 (Performance Metrics) ──\n", + "perf_rows = []\n", + "for name in STRATEGY_NAMES:\n", + " r_s = pd.Series(daily_pnl[name][LOOKBACK:], index=dates[LOOKBACK:])\n", + " ann_ret = (1 + r_s).prod() ** (FREQ / len(r_s)) - 1 # 年化收益率 (Ann. return)\n", + " ann_vol = r_s.std() * np.sqrt(FREQ) # 年化波动率 (Ann. vol)\n", + " sharpe = (ann_ret - RF) / ann_vol if ann_vol > 0 else 0\n", + " nav_s = (1 + r_s).cumprod()\n", + " max_dd = (nav_s / nav_s.cummax() - 1).min() # 最大回撤 (Max drawdown)\n", + " calmar = ann_ret / (-max_dd) if max_dd < 0 else 99.0 # Calmar 比率\n", + " perf_rows.append({\n", + " \"策略\": name, \"年化收益\": ann_ret, \"年化波动\": ann_vol,\n", + " \"夏普比率\": sharpe, \"最大回撤\": max_dd, \"Calmar\": calmar\n", + " })\n", + "\n", + "perf_df = pd.DataFrame(perf_rows).set_index(\"策略\")\n", + "\n", + "print(f\"\\n 策略对比 / Strategy Comparison:\")\n", + "print(f\" {'策略':<28} {'年化收益':>8} {'年化波动':>8} {'夏普':>7} {'最大回撤':>9} {'Calmar':>8}\")\n", + "print(f\" {'─' * 65}\")\n", + "for name, row in perf_df.iterrows():\n", + " print(f\" {name:<28} {row['年化收益']:>8.1%} {row['年化波动']:>8.1%} \"\n", + " f\"{row['夏普比率']:>7.3f} {row['最大回撤']:>9.1%} {min(row['Calmar'], 99.0):>8.2f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "0c16a50a", + "metadata": {}, + "source": [ + "# ══════════════════════════════════════════════════════════════════════\n", + "# §8 可视化:9 图综合面板\n", + "# 9-Panel Visualization Dashboard\n", + "\n", + "# ══════════════════════════════════════════════════════════════════════" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b16f0192", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"\\n[§8] 生成可视化图表 / Generating visualization...\")\n", + "\n", + "# 颜色配置 / Color palette\n", + "COLORS = {\n", + " \"等权 EW\": \"#888888\",\n", + " \"最小方差 MinVar\": \"#2196F3\",\n", + " \"最大夏普 MaxSharpe\": \"#FF5722\",\n", + " \"风险平价 RP\": \"#4CAF50\",\n", + " \"Black-Litterman BL\": \"#9C27B0\",\n", + "}\n", + "PCT_FMT = FuncFormatter(lambda x, _: f\"{x:.0%}\")\n", + "PCT1_FMT = FuncFormatter(lambda x, _: f\"{x:.1%}\")\n", + "DARK_BG = '#1A1D27'\n", + "LT = '#E0E0E0' # 浅色文字 (Light text)\n", + "GRID_C = '#2A2D3A' # 网格颜色 (Grid color)\n", + "\n", + "fig = plt.figure(figsize=(22, 22), facecolor='#0F1117')\n", + "fig.suptitle(\n", + " \"量化组合优化演示 / Portfolio Optimization Demo\",\n", + " fontsize=18, color='white', y=0.98, fontweight='bold'\n", + ")\n", + "gs = gridspec.GridSpec(3, 3, figure=fig, hspace=0.42, wspace=0.32)\n", + "\n", + "ax1 = fig.add_subplot(gs[0, 0]) # 有效前沿 Efficient Frontier\n", + "ax2 = fig.add_subplot(gs[0, 1]) # 组合权重堆叠 Portfolio Weights\n", + "ax3 = fig.add_subplot(gs[0, 2]) # 行业权重对比 Sector Weights\n", + "ax4 = fig.add_subplot(gs[1, 0]) # 净值曲线 NAV Curves\n", + "ax5 = fig.add_subplot(gs[1, 1]) # 风险贡献 Risk Contribution (RP)\n", + "ax6 = fig.add_subplot(gs[1, 2]) # 滚动夏普 Rolling Sharpe\n", + "ax7 = fig.add_subplot(gs[2, 0]) # 月度收益箱形图 Monthly Return Box\n", + "ax8 = fig.add_subplot(gs[2, 1]) # 相关系数热力图 Correlation Heatmap\n", + "ax9 = fig.add_subplot(gs[2, 2]) # 绩效汇总表 Performance Table\n", + "\n", + "for ax in [ax1, ax2, ax3, ax4, ax5, ax6, ax7, ax8, ax9]:\n", + " ax.set_facecolor(DARK_BG)\n", + " ax.tick_params(colors=LT, labelsize=8)\n", + " for sp in ax.spines.values():\n", + " sp.set_edgecolor(GRID_C)\n", + "\n", + "# ── 图1:有效前沿 + 随机组合云 + 关键节点 ──\n", + "# Efficient Frontier + Monte Carlo cloud + key portfolios\n", + "ax1.set_title(\"有效前沿 Efficient Frontier\", color=LT, fontsize=10, pad=6)\n", + "ax1.scatter(rand_vols, rand_rets,\n", + " c=rand_sharpes, cmap='RdYlGn', vmin=0, vmax=2.5,\n", + " alpha=0.25, s=6, zorder=1)\n", + "ax1.plot(frontier_vols, frontier_rets, '-', color='#FFD700', lw=2.2, zorder=4,\n", + " label='有效前沿')\n", + "# 资本市场线 (Capital Market Line, CML) — 从无风险利率到最大夏普组合并延伸\n", + "cml_x = np.linspace(0, vol_msr * 1.4, 50)\n", + "cml_y = RF + sr_msr * cml_x\n", + "ax1.plot(cml_x, cml_y, '--', color='#FFEB3B', lw=1.0, alpha=0.7,\n", + " zorder=3, label='资本市场线 CML')\n", + "# 关键组合点\n", + "specials = [\n", + " (\"最小方差\\nMinVar\", vol_min, ret_min, COLORS[\"最小方差 MinVar\"]),\n", + " (\"最大夏普\\nMaxSharpe\",vol_msr, ret_msr, COLORS[\"最大夏普 MaxSharpe\"]),\n", + " (\"等权\\nEW\", vol_eq, ret_eq, COLORS[\"等权 EW\"]),\n", + " (\"风险平价\\nRP\", vol_rp, ret_rp, COLORS[\"风险平价 RP\"]),\n", + " (\"BL\", vol_bl, ret_bl, COLORS[\"Black-Litterman BL\"]),\n", + "]\n", + "for label, v, r, c in specials:\n", + " ax1.scatter(v, r, s=100, color=c, zorder=6, edgecolors='white', lw=0.8)\n", + " ax1.annotate(label, (v, r), color=LT, fontsize=6,\n", + " xytext=(5, 3), textcoords='offset points')\n", + "# 无风险利率点 (Risk-free rate point)\n", + "ax1.scatter(0, RF, s=80, color='yellow', zorder=6, marker='*')\n", + "ax1.annotate(f\"无风险\\nRf={RF:.0%}\", (0, RF), color='yellow', fontsize=6,\n", + " xytext=(4, 2), textcoords='offset points')\n", + "ax1.xaxis.set_major_formatter(PCT_FMT)\n", + "ax1.yaxis.set_major_formatter(PCT_FMT)\n", + "ax1.set_xlabel(\"年化波动率 Volatility\", color=LT, fontsize=8)\n", + "ax1.set_ylabel(\"年化收益率 Return\", color=LT, fontsize=8)\n", + "ax1.legend(fontsize=7, labelcolor=LT, facecolor=DARK_BG, edgecolor=GRID_C)\n", + "ax1.grid(color=GRID_C, alpha=0.5)\n", + "\n", + "# ── 图2:各策略权重堆叠柱状图 ──\n", + "# Stacked bar chart of portfolio weights\n", + "ax2.set_title(\"组合权重 Portfolio Weights\", color=LT, fontsize=10, pad=6)\n", + "weight_order = [\"EW\", \"MinVar\", \"MaxSharpe\", \"RP\", \"BL\"]\n", + "weight_arrays = [w_eq.values, w_minvar.values, w_maxsharpe.values, w_rp.values, w_bl.values]\n", + "bar_colors = plt.cm.tab20(np.linspace(0, 1, N_STOCKS))\n", + "bottoms = np.zeros(len(weight_order))\n", + "x_pos = np.arange(len(weight_order))\n", + "for i in range(N_STOCKS):\n", + " heights = [wa[i] for wa in weight_arrays]\n", + " ax2.bar(x_pos, heights, bottom=bottoms, color=bar_colors[i], width=0.72)\n", + " bottoms += heights\n", + "ax2.set_xticks(x_pos)\n", + "ax2.set_xticklabels(weight_order, color=LT, fontsize=9)\n", + "ax2.yaxis.set_major_formatter(PCT_FMT)\n", + "ax2.set_ylabel(\"权重 Weight\", color=LT, fontsize=8)\n", + "ax2.grid(axis='y', color=GRID_C, alpha=0.5)\n", + "\n", + "# ── 图3:行业权重对比(分组柱状图)──\n", + "# Grouped bar chart: sector weights per strategy\n", + "ax3.set_title(\"行业权重对比 Sector Weights\", color=LT, fontsize=10, pad=6)\n", + "x_sec = np.arange(N_SECTORS)\n", + "bw = 0.15\n", + "strat_colors_list = list(COLORS.values())\n", + "for k, (wname, warr) in enumerate(zip(weight_order, weight_arrays)):\n", + " sec_wts = [np.sum(warr[SECTORS[s]]) for s in SECTOR_NAMES]\n", + " off = (k - len(weight_order) / 2 + 0.5) * bw\n", + " ax3.bar(x_sec + off, sec_wts, width=bw,\n", + " color=strat_colors_list[k], label=wname, alpha=0.85)\n", + "ax3.set_xticks(x_sec)\n", + "ax3.set_xticklabels(SECTOR_SHORT, color=LT, fontsize=8)\n", + "ax3.yaxis.set_major_formatter(PCT_FMT)\n", + "ax3.axhline(MAX_SECTOR_WT, ls='--', color='#FF7043', lw=0.8, alpha=0.6,\n", + " label=f'上限 {MAX_SECTOR_WT:.0%}')\n", + "ax3.legend(fontsize=6, labelcolor=LT, facecolor=DARK_BG, edgecolor=GRID_C,\n", + " ncol=2, loc='upper right')\n", + "ax3.grid(axis='y', color=GRID_C, alpha=0.5)\n", + "\n", + "# ── 图4:净值曲线 ──\n", + "ax4.set_title(\"净值曲线 NAV Curves\", color=LT, fontsize=10, pad=6)\n", + "for name in STRATEGY_NAMES:\n", + " is_key = \"MaxSharpe\" in name or \"BL\" in name\n", + " ax4.plot(nav_df.index, nav_df[name],\n", + " color=COLORS[name], lw=2.0 if is_key else 1.2,\n", + " alpha=1.0 if is_key else 0.7,\n", + " label=name.split(\" \")[0])\n", + "ax4.set_ylabel(\"净值 NAV\", color=LT, fontsize=8)\n", + "ax4.legend(fontsize=7, labelcolor=LT, facecolor=DARK_BG,\n", + " edgecolor=GRID_C, ncol=2)\n", + "ax4.grid(color=GRID_C, alpha=0.5)\n", + "ax4.xaxis.set_tick_params(rotation=20)\n", + "\n", + "# ── 图5:风险平价组合的风险贡献分解 ──\n", + "# Risk contribution breakdown for Risk Parity portfolio\n", + "ax5.set_title(\"风险贡献分布 Risk Contribution (RP)\", color=LT, fontsize=10, pad=6)\n", + "rc_sorted = pd.Series(rc_pct_rp * 100, index=stock_names).sort_values(ascending=False)\n", + "bar_c = ['#4CAF50' if abs(v / 100 - 1.0 / N_STOCKS) < 5e-3\n", + " else '#FF7043' for v in rc_sorted]\n", + "ax5.bar(range(N_STOCKS), rc_sorted.values, color=bar_c, alpha=0.85)\n", + "ax5.axhline(100.0 / N_STOCKS, color='white', lw=1.2, ls='--',\n", + " label=f'目标 1/N={100/N_STOCKS:.1f}%')\n", + "ax5.set_xticks(range(N_STOCKS))\n", + "ax5.set_xticklabels(rc_sorted.index, rotation=45, fontsize=6, color=LT)\n", + "ax5.set_ylabel(\"风险贡献 RC%\", color=LT, fontsize=8)\n", + "ax5.legend(fontsize=7, labelcolor=LT, facecolor=DARK_BG, edgecolor=GRID_C)\n", + "ax5.grid(axis='y', color=GRID_C, alpha=0.5)\n", + "\n", + "# ── 图6:滚动夏普比率(63 日窗口)──\n", + "# Rolling Sharpe ratio (63-day window)\n", + "ax6.set_title(\"滚动夏普比率 Rolling Sharpe (63D)\", color=LT, fontsize=10, pad=6)\n", + "ROLL_WIN = 63\n", + "for name in STRATEGY_NAMES:\n", + " r_s = pd.Series(daily_pnl[name], index=dates)\n", + " roll_sr = r_s.rolling(ROLL_WIN).apply(\n", + " lambda x: (x.mean() * FREQ - RF) / (x.std() * np.sqrt(FREQ) + 1e-10)\n", + " )\n", + " ax6.plot(dates, roll_sr, color=COLORS[name], lw=1.0,\n", + " alpha=0.85, label=name.split(\" \")[0])\n", + "ax6.axhline(0, color='white', lw=0.7, ls='--')\n", + "ax6.set_ylabel(\"夏普比率 Sharpe\", color=LT, fontsize=8)\n", + "ax6.legend(fontsize=7, labelcolor=LT, facecolor=DARK_BG,\n", + " edgecolor=GRID_C, ncol=2)\n", + "ax6.grid(color=GRID_C, alpha=0.5)\n", + "ax6.xaxis.set_tick_params(rotation=20)\n", + "\n", + "# ── 图7:月度收益分布箱形图 ──\n", + "# Monthly return distribution boxplot\n", + "ax7.set_title(\"月度收益分布 Monthly Return Distribution\", color=LT, fontsize=10, pad=6)\n", + "monthly_rets = {}\n", + "for name in STRATEGY_NAMES:\n", + " r_s = pd.Series(daily_pnl[name], index=dates)\n", + " monthly_rets[name.split(\" \")[0]] = r_s.resample('M').apply(\n", + " lambda x: (1 + x).prod() - 1\n", + " ).values\n", + "\n", + "bp = ax7.boxplot(\n", + " list(monthly_rets.values()),\n", + " patch_artist=True,\n", + " labels=list(monthly_rets.keys()),\n", + " medianprops=dict(color='white', lw=1.5),\n", + " whiskerprops=dict(color=LT),\n", + " capprops=dict(color=LT),\n", + " flierprops=dict(marker='.', color='#888888', ms=3)\n", + ")\n", + "for patch, c in zip(bp['boxes'], list(COLORS.values())):\n", + " patch.set_facecolor(c)\n", + " patch.set_alpha(0.72)\n", + "ax7.yaxis.set_major_formatter(PCT1_FMT)\n", + "ax7.axhline(0, color='white', lw=0.6, ls='--')\n", + "ax7.set_xticklabels(list(monthly_rets.keys()), color=LT, fontsize=8, rotation=15)\n", + "ax7.grid(axis='y', color=GRID_C, alpha=0.5)\n", + "\n", + "# ── 图8:收益相关系数热力图(含行业分割线)──\n", + "# Return correlation heatmap with sector dividers\n", + "ax8.set_title(\"收益相关系数 Return Correlation Matrix\", color=LT, fontsize=10, pad=6)\n", + "corr = ret_df.corr().values\n", + "im = ax8.imshow(corr, cmap='RdYlGn', vmin=-0.2, vmax=1.0, aspect='auto')\n", + "ax8.set_xticks(range(N_STOCKS))\n", + "ax8.set_yticks(range(N_STOCKS))\n", + "ax8.set_xticklabels(stock_names, rotation=90, fontsize=5.5, color=LT)\n", + "ax8.set_yticklabels(stock_names, fontsize=5.5, color=LT)\n", + "# 行业分割线 / Sector boundary lines\n", + "sector_bounds = [0, 5, 9, 13, 17, 20]\n", + "for b in sector_bounds[1:-1]:\n", + " ax8.axhline(b - 0.5, color='white', lw=0.9, alpha=0.8)\n", + " ax8.axvline(b - 0.5, color='white', lw=0.9, alpha=0.8)\n", + "cb = plt.colorbar(im, ax=ax8, fraction=0.046, pad=0.04)\n", + "cb.ax.tick_params(colors=LT, labelsize=7)\n", + "\n", + "# ── 图9:绩效汇总表 ──\n", + "# Performance summary table\n", + "ax9.axis('off')\n", + "ax9.set_title(\"绩效汇总 Performance Summary\", color=LT, fontsize=10, pad=6)\n", + "col_labels = [\"年化收益\\nAnn.Ret\", \"年化波动\\nAnn.Vol\",\n", + " \"夏普比率\\nSharpe\", \"最大回撤\\nMax DD\", \"Calmar\\n比率\"]\n", + "row_labels = [n.split(\" \")[0] for n in STRATEGY_NAMES]\n", + "cell_data = []\n", + "for name in STRATEGY_NAMES:\n", + " r = perf_df.loc[name]\n", + " cell_data.append([\n", + " f\"{r['年化收益']:+.1%}\",\n", + " f\"{r['年化波动']:.1%}\",\n", + " f\"{r['夏普比率']:.2f}\",\n", + " f\"{r['最大回撤']:.1%}\",\n", + " f\"{min(r['Calmar'], 99.0):.2f}\",\n", + " ])\n", + "\n", + "tbl = ax9.table(cellText=cell_data, rowLabels=row_labels,\n", + " colLabels=col_labels, loc='center', cellLoc='center')\n", + "tbl.auto_set_font_size(False)\n", + "tbl.set_fontsize(8)\n", + "tbl.scale(1.12, 2.0)\n", + "for (row, col), cell in tbl.get_celld().items():\n", + " cell.set_facecolor(DARK_BG)\n", + " cell.set_edgecolor(GRID_C)\n", + " cell.set_text_props(color=LT)\n", + " if row == 0 or col == -1:\n", + " cell.set_facecolor('#2A2D3A')\n", + " cell.set_text_props(color='#FFD700', fontweight='bold')\n", + "\n", + "# 保存图表 / Save figure\n", + "OUTPUT_FILE = \"portfolio_optimization_demo.png\"\n", + "plt.savefig(OUTPUT_FILE, dpi=150, bbox_inches='tight',\n", + " facecolor=fig.get_facecolor())\n", + "\n", + "print(f\" 图表已保存: {OUTPUT_FILE} / Chart saved: {OUTPUT_FILE}\")\n", + "\n", + "print(\"\\n\" + \"=\" * 70)\n", + "print(\" 演示完成! Demo Complete!\")\n", + "print(f\" 输出: {OUTPUT_FILE}\")\n", + "print(\"=\" * 70)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (trading)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/quant_strategy_backtest_demo.ipynb b/quant_strategy_backtest_demo.ipynb new file mode 100644 index 0000000..aaf2bb7 --- /dev/null +++ b/quant_strategy_backtest_demo.ipynb @@ -0,0 +1,1093 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fb3aacb0", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# Quantitative Trading — Strategy Development & Backtesting Demo\n", + "# 量化交易 — 策略开发与回测演示\n", + "\n", + "# =============================================================================\n", + "#\n", + "# 本文件是数据管道 (quant_data_pipeline_demo.py) 的续集。\n", + "# This file is the sequel to the data pipeline demo.\n", + "#\n", + "# Topics covered / 涵盖主题:\n", + "# 1. Technical Indicators 技术指标 (MA, RSI, MACD, Bollinger Bands)\n", + "# 2. Signal Generation 信号生成 (entry & exit rules)\n", + "# 3. Two Demo Strategies 两个示范策略:\n", + "# A. Dual Moving Average Crossover 双均线金叉死叉策略\n", + "# B. RSI Mean Reversion RSI 均值回归策略\n", + "# 4. Vectorized Backtest Engine 向量化回测引擎\n", + "# 5. Performance Metrics 绩效指标\n", + "# (Sharpe, Sortino, Max Drawdown, Win Rate …)\n", + "# 6. Visualization 可视化\n", + "#\n", + "# Prerequisites / 前置条件:\n", + "# pip install numpy pandas matplotlib scipy\n", + "#\n", + "# Running / 运行方式:\n", + "# python quant_strategy_backtest_demo.py\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "156c36ec", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.gridspec as gridspec\n", + "from scipy import stats\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "# 中文字体配置 / Chinese font config\n", + "plt.rcParams['font.sans-serif'] = ['WenQuanYi Zen Hei', 'Arial Unicode MS', 'SimHei', 'DejaVu Sans']\n", + "plt.rcParams['axes.unicode_minus'] = False\n", + "\n", + "np.random.seed(42)\n", + "print(\"=\" * 70)\n", + "print(\" 量化交易策略开发与回测演示\")\n", + "print(\" Quantitative Trading: Strategy Development & Backtesting Demo\")\n", + "print(\"=\" * 70)" + ] + }, + { + "cell_type": "markdown", + "id": "62cbe290", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 0: Synthetic Price Data 合成价格数据\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# We simulate a single stock using Geometric Brownian Motion (几何布朗运动),\n", + "# the classical model that underlies the Black-Scholes formula.\n", + "#\n", + "# GBM formula:\n", + "# dS = μ·S·dt + σ·S·dW\n", + "#\n", + "# Discrete form (what we actually compute each day):\n", + "# S_t = S_{t-1} · exp( (μ - σ²/2)·dt + σ·√dt·ε )\n", + "#\n", + "# where:\n", + "# μ = drift / 年化漂移率 (expected annual return)\n", + "# σ = volatility / 年化波动率\n", + "# dt = 1/252 (one trading day as a fraction of a year)\n", + "# ε ~ N(0,1) (standard normal random shock / 标准正态随机扰动)\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04ed6429", + "metadata": {}, + "outputs": [], + "source": [ + "def generate_price_series(\n", + " n_days: int = 1500,\n", + " mu: float = 0.10, # 年化预期收益率 / annual expected return\n", + " sigma: float = 0.25, # 年化波动率 / annual volatility\n", + " s0: float = 100.0, # 初始价格 / initial price\n", + " seed: int = 42,\n", + ") -> pd.Series:\n", + " \"\"\"\n", + " Generate a synthetic daily price series via GBM.\n", + " 用几何布朗运动生成合成日线价格序列。\n", + " \"\"\"\n", + " np.random.seed(seed)\n", + " dt = 1.0 / 252 # 每个交易日占一年的比例\n", + " epsilon = np.random.randn(n_days) # 每日随机冲击\n", + " log_returns = (mu - 0.5 * sigma ** 2) * dt + sigma * np.sqrt(dt) * epsilon\n", + " prices = s0 * np.exp(np.cumsum(log_returns)) # 累积乘积 → 价格路径\n", + "\n", + " # 生成工作日日期序列 / generate business-day date index\n", + " dates = pd.bdate_range(start=\"2019-01-02\", periods=n_days)\n", + " return pd.Series(prices, index=dates, name=\"close\")\n", + "\n", + "\n", + "price = generate_price_series()\n", + "print(f\"\\n[数据] 生成模拟股票价格: {len(price)} 个交易日\")\n", + "print(f\" 价格区间: {price.min():.2f} ~ {price.max():.2f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "ccda8a1f", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 1: Technical Indicators 技术指标\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# Technical indicators transform raw price/volume data into signals.\n", + "# 技术指标将原始价格/成交量数据转化为交易信号。\n", + "#\n", + "# They are divided into two broad families:\n", + "# 主要分为两大类:\n", + "#\n", + "# ① Trend-following indicators 趋势跟随指标\n", + "# → Moving Averages (MA), MACD\n", + "# → Work well in trending markets (趋势市中效果好)\n", + "#\n", + "# ② Oscillators / Mean-reversion indicators 震荡/均值回归指标\n", + "# → RSI, Bollinger Bands\n", + "# → Work well in range-bound / choppy markets (震荡市中效果好)\n", + "\n", + "# =============================================================================\n", + "\n", + "# ── 1-A Simple Moving Average 简单移动平均线 (SMA) ──────────────────────────\n", + "#\n", + "# SMA_n(t) = (P_{t} + P_{t-1} + … + P_{t-n+1}) / n\n", + "#\n", + "# The SMA smooths out daily noise to reveal the underlying trend.\n", + "# SMA 平滑日内噪音,揭示潜在趋势。\n", + "# A longer window → smoother, but lags more behind recent price action.\n", + "# 窗口越长 → 越平滑,但对价格变化的反应越滞后。" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ec5acb4", + "metadata": {}, + "outputs": [], + "source": [ + "def sma(prices: pd.Series, window: int) -> pd.Series:\n", + " \"\"\"Simple Moving Average / 简单移动平均线\"\"\"\n", + " return prices.rolling(window=window).mean()\n", + "\n", + "\n", + "# ── 1-B Exponential Moving Average 指数移动平均线 (EMA) ───────────────────\n", + "#\n", + "# EMA gives MORE weight to recent prices (recent data matters more).\n", + "# EMA 给予近期价格更高权重(近期数据更重要)。\n", + "#\n", + "# EMA_t = α · P_t + (1 - α) · EMA_{t-1}\n", + "# where α = 2 / (n + 1) (smoothing factor / 平滑因子)\n", + "#\n", + "# EMA reacts faster than SMA to price changes.\n", + "# EMA 对价格变动的反应比 SMA 更灵敏。\n", + "\n", + "def ema(prices: pd.Series, span: int) -> pd.Series:\n", + " \"\"\"Exponential Moving Average / 指数移动平均线\"\"\"\n", + " return prices.ewm(span=span, adjust=False).mean()\n", + "\n", + "\n", + "# ── 1-C RSI 相对强弱指数 (Relative Strength Index) ─────────────────────────\n", + "#\n", + "# RSI measures the speed and magnitude of recent price changes.\n", + "# RSI 衡量近期价格变动的速度和幅度。\n", + "#\n", + "# Formula:\n", + "# RS = average_gain / average_loss (over last n days)\n", + "# RSI = 100 - 100 / (1 + RS)\n", + "#\n", + "# Interpretation / 指标解读:\n", + "# RSI > 70 → Overbought 超买 (price may be due for a pullback / 价格可能回调)\n", + "# RSI < 30 → Oversold 超卖 (price may be due for a bounce / 价格可能反弹)\n", + "# RSI = 50 → Neutral 中性\n", + "\n", + "def rsi(prices: pd.Series, window: int = 14) -> pd.Series:\n", + " \"\"\"\n", + " Compute Wilder's RSI.\n", + " 计算 Wilder 平滑法 RSI。\n", + " \"\"\"\n", + " delta = prices.diff() # 每日价格变化 / daily price change\n", + " gain = delta.clip(lower=0) # 只保留上涨部分 / keep only up-days\n", + " loss = -delta.clip(upper=0) # 只保留下跌部分 / keep only down-days\n", + "\n", + " # Wilder uses EMA with span = 2*n - 1 (equivalent to 1/n smoothing)\n", + " avg_gain = gain.ewm(alpha=1.0 / window, adjust=False).mean()\n", + " avg_loss = loss.ewm(alpha=1.0 / window, adjust=False).mean()\n", + "\n", + " rs = avg_gain / avg_loss # 相对强弱值 / relative strength\n", + " return 100 - (100 / (1 + rs)) # 转换为 0~100 范围\n", + "\n", + "\n", + "# ── 1-D MACD 指数平滑异同移动平均线 ────────────────────────────────────────\n", + "#\n", + "# MACD reveals the relationship between two EMAs.\n", + "# MACD 揭示两条 EMA 之间的关系。\n", + "#\n", + "# Components / 构成:\n", + "# MACD Line MACD线 = EMA(12) - EMA(26) (fast minus slow / 快线减慢线)\n", + "# Signal Line 信号线 = EMA(9) of MACD Line (trigger line / 触发线)\n", + "# Histogram 柱状图 = MACD Line - Signal Line\n", + "#\n", + "# Trading rules / 交易规则:\n", + "# MACD crosses above Signal → Bullish (金叉, buy signal / 买入信号)\n", + "# MACD crosses below Signal → Bearish (死叉, sell signal / 卖出信号)\n", + "\n", + "def macd(prices: pd.Series,\n", + " fast: int = 12, slow: int = 26, signal: int = 9\n", + " ) -> pd.DataFrame:\n", + " \"\"\"\n", + " Compute MACD, Signal line, and Histogram.\n", + " 计算 MACD线、信号线和柱状图。\n", + " \"\"\"\n", + " ema_fast = ema(prices, fast)\n", + " ema_slow = ema(prices, slow)\n", + " macd_line = ema_fast - ema_slow # MACD 线\n", + " signal_line = ema(macd_line, signal) # 信号线 (DIF的EMA)\n", + " histogram = macd_line - signal_line # 柱状图 (MACD Bar)\n", + " return pd.DataFrame({\n", + " \"macd\": macd_line,\n", + " \"signal\": signal_line,\n", + " \"histogram\": histogram,\n", + " })\n", + "\n", + "\n", + "# ── 1-E Bollinger Bands 布林带 ─────────────────────────────────────────────\n", + "#\n", + "# Bollinger Bands place upper/lower envelopes around a moving average.\n", + "# 布林带在移动平均线上下各画一条\"包络线\"。\n", + "#\n", + "# Formula:\n", + "# Middle Band 中轨 = SMA(n)\n", + "# Upper Band 上轨 = SMA(n) + k·σ_n (k = 2 by default / 默认 k=2)\n", + "# Lower Band 下轨 = SMA(n) - k·σ_n\n", + "#\n", + "# where σ_n is the rolling standard deviation / 滚动标准差\n", + "#\n", + "# When price touches the lower band → oversold area (超卖区域)\n", + "# When price touches the upper band → overbought area (超买区域)\n", + "# Band width (带宽) contracts before explosive moves (波动收窄常预示突破)\n", + "\n", + "def bollinger_bands(prices: pd.Series, window: int = 20, k: float = 2.0\n", + " ) -> pd.DataFrame:\n", + " \"\"\"\n", + " Compute Bollinger Bands.\n", + " 计算布林带(上轨、中轨、下轨)。\n", + " \"\"\"\n", + " mid = sma(prices, window) # 中轨 (SMA)\n", + " std = prices.rolling(window).std() # 滚动标准差\n", + " upper = mid + k * std # 上轨\n", + " lower = mid - k * std # 下轨\n", + " # %B indicator: where is the current price within the band?\n", + " # %B 指标:当前价格在带宽中的位置 (0=下轨, 1=上轨)\n", + " pct_b = (prices - lower) / (upper - lower)\n", + " return pd.DataFrame({\n", + " \"upper\": upper, \"mid\": mid, \"lower\": lower, \"pct_b\": pct_b\n", + " })\n", + "\n", + "\n", + "# Compute all indicators on our simulated price series\n", + "# 对模拟价格序列计算所有指标\n", + "sma20 = sma(price, 20) # 20日均线 / 20-day SMA\n", + "sma60 = sma(price, 60) # 60日均线 / 60-day SMA (longer trend)\n", + "rsi14 = rsi(price, 14) # 14日RSI / 14-day RSI\n", + "macd_df = macd(price) # MACD (12/26/9)\n", + "bb = bollinger_bands(price, window=20, k=2.0)\n", + "\n", + "print(\"\\n[指标] 技术指标计算完成:\")\n", + "print(f\" SMA20 — 首个有效值日期: {sma20.first_valid_index().date()}\")\n", + "print(f\" SMA60 — 首个有效值日期: {sma60.first_valid_index().date()}\")\n", + "print(f\" RSI14 — 首个有效值日期: {rsi14.first_valid_index().date()}\")\n", + "print(f\" MACD — 首个有效值日期: {macd_df['macd'].first_valid_index().date()}\")\n", + "print(f\" BollingerBands — 首个有效值日期: {bb['mid'].first_valid_index().date()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "739084bb", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 2: Strategy A — Dual Moving Average Crossover\n", + "# 策略 A — 双均线金叉/死叉策略\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# One of the oldest and most intuitive trend-following strategies.\n", + "# 最古老也最直观的趋势跟随策略之一。\n", + "#\n", + "# Logic / 逻辑:\n", + "# Golden Cross (金叉): short MA crosses ABOVE long MA → BUY (做多)\n", + "# Death Cross (死叉): short MA crosses BELOW long MA → SELL (平仓)\n", + "#\n", + "# Rationale / 原理:\n", + "# When the short-term average rises above the long-term average, it signals\n", + "# that recent momentum is stronger than the historical trend → bullish.\n", + "# 短期均线上穿长期均线,意味着近期动能强于历史趋势 → 看涨。\n", + "#\n", + "# Parameters / 参数:\n", + "# SHORT_WINDOW = 20 (fast line / 快线)\n", + "# LONG_WINDOW = 60 (slow line / 慢线)\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a99d5899", + "metadata": {}, + "outputs": [], + "source": [ + "SHORT_WIN = 20 # 短期均线窗口 / short-term MA window\n", + "LONG_WIN = 60 # 长期均线窗口 / long-term MA window\n", + "\n", + "ma_short = sma(price, SHORT_WIN)\n", + "ma_long = sma(price, LONG_WIN)\n", + "\n", + "# ── Signal generation 信号生成 ───────────────────────────────────────────────\n", + "#\n", + "# Signal (信号) = +1 when we should be LONG (持多仓), 0 when out of market (空仓)\n", + "#\n", + "# Step 1: raw_signal = 1 whenever short MA > long MA (short MA above long MA)\n", + "# Step 2: detect crossovers (cross = today's signal ≠ yesterday's signal)\n", + "#\n", + "# We use a \"position\" approach — hold the position until it reverses.\n", + "# 使用\"持仓\"方式 — 持有直到信号翻转。\n", + "\n", + "# raw_signal: 1 = short above long (看多区域), 0 = short below long (看空区域)\n", + "raw_signal = (ma_short > ma_long).astype(int)\n", + "\n", + "# Align signals: use yesterday's signal to trade today (avoid lookahead bias)\n", + "# 用昨天的信号决定今天的仓位,避免\"未来数据偷窥\" (前视偏差 / lookahead bias)\n", + "ma_signal = raw_signal.shift(1).fillna(0)\n", + "\n", + "print(\"\\n[策略A] 双均线信号生成完成\")\n", + "print(f\" 多头持仓天数 (Signal=1): {int(ma_signal.sum())} 天\")\n", + "print(f\" 空仓天数 (Signal=0): {int((ma_signal == 0).sum())} 天\")" + ] + }, + { + "cell_type": "markdown", + "id": "dd6312ac", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 3: Strategy B — RSI Mean Reversion\n", + "# 策略 B — RSI 均值回归策略\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# This is a contrarian strategy: buy when the market seems \"too weak\",\n", + "# sell when it seems \"too strong\".\n", + "# 这是一个逆势策略:市场\"跌过头\"时买入,\"涨过头\"时卖出。\n", + "#\n", + "# Logic / 逻辑:\n", + "# RSI drops below oversold level (超卖线, default 30) → BUY signal\n", + "# RSI rises above overbought level (超买线, default 70) → SELL signal\n", + "#\n", + "# This exploits mean reversion (均值回归): extreme prices tend to revert.\n", + "# 利用均值回归特性:极端价格倾向于回归均值。\n", + "#\n", + "# Risk / 风险:\n", + "# In a strong trend, RSI can stay oversold/overbought for long stretches.\n", + "# 在强趋势中,RSI 可以长时间停留在超卖/超买区域,造成连续亏损。\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c428d624", + "metadata": {}, + "outputs": [], + "source": [ + "RSI_OVERSOLD = 30 # 超卖线 / oversold threshold\n", + "RSI_OVERBOUGHT = 70 # 超买线 / overbought threshold\n", + "\n", + "def rsi_signal(rsi_series: pd.Series,\n", + " oversold: float = 30,\n", + " overbought: float = 70) -> pd.Series:\n", + " \"\"\"\n", + " Generate long/short/flat signals from RSI.\n", + " 根据 RSI 生成多空平信号。\n", + "\n", + " Returns a Series of:\n", + " +1 → Long (做多)\n", + " -1 → Short (做空)\n", + " 0 → Flat (空仓, no position)\n", + " \"\"\"\n", + " position = pd.Series(0, index=rsi_series.index, dtype=float)\n", + " current_pos = 0 # 当前持仓状态 / current position state\n", + "\n", + " for i in range(1, len(rsi_series)):\n", + " r = rsi_series.iloc[i]\n", + " if pd.isna(r):\n", + " position.iloc[i] = 0\n", + " continue\n", + "\n", + " # Entry rules / 入场规则\n", + " if r < oversold and current_pos == 0:\n", + " current_pos = 1 # 超卖 → 做多 / oversold → go long\n", + "\n", + " elif r > overbought and current_pos == 0:\n", + " current_pos = -1 # 超买 → 做空 / overbought → go short\n", + "\n", + " # Exit rules / 出场规则\n", + " # Exit long when RSI recovers above 50 (回到中性区域 / back to neutral)\n", + " elif current_pos == 1 and r > 50:\n", + " current_pos = 0\n", + "\n", + " # Exit short when RSI falls below 50\n", + " elif current_pos == -1 and r < 50:\n", + " current_pos = 0\n", + "\n", + " position.iloc[i] = current_pos\n", + "\n", + " return position\n", + "\n", + "\n", + "rsi_pos = rsi_signal(rsi14, RSI_OVERSOLD, RSI_OVERBOUGHT)\n", + "\n", + "# Shift by 1 day to avoid lookahead bias / 前移一天避免前视偏差\n", + "rsi_signal_shifted = rsi_pos.shift(1).fillna(0)\n", + "\n", + "print(\"\\n[策略B] RSI信号生成完成\")\n", + "print(f\" 多头持仓天数 (Signal=+1): {int((rsi_signal_shifted == 1).sum())} 天\")\n", + "print(f\" 空头持仓天数 (Signal=-1): {int((rsi_signal_shifted == -1).sum())} 天\")\n", + "print(f\" 空仓天数 (Signal= 0): {int((rsi_signal_shifted == 0).sum())} 天\")" + ] + }, + { + "cell_type": "markdown", + "id": "3bb7e0d4", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 4: Vectorized Backtest Engine 向量化回测引擎\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# A backtest (回测) simulates how a strategy would have performed\n", + "# on historical data. It is the primary tool for validating a strategy\n", + "# before risking real money.\n", + "# 回测是在历史数据上模拟策略表现的工具,是真实投资前验证策略的主要手段。\n", + "#\n", + "# Two main backtest styles / 两种主要回测方式:\n", + "#\n", + "# ① Vectorized backtest 向量化回测\n", + "# - Compute all positions & P&L as array operations at once (numpy/pandas)\n", + "# - Very fast; good for strategy exploration\n", + "# - 所有仓位和盈亏一次性用数组运算计算,速度极快,适合策略探索\n", + "#\n", + "# ② Event-driven backtest 事件驱动回测\n", + "# - Simulate time step-by-step, reacting to each market event\n", + "# - More realistic (handles fills, slippage, latency, order queuing)\n", + "# - 逐笔模拟市场事件,更真实(考虑成交、滑点、延迟等),速度较慢\n", + "#\n", + "# We use the vectorized approach here for clarity and speed.\n", + "# 此处使用向量化方式,兼顾清晰度和速度。\n", + "#\n", + "# Cost model 交易成本模型:\n", + "# - Commission (佣金): charged each time you trade (per trade)\n", + "# - Slippage (滑点): the difference between the expected fill price and\n", + "# the actual fill price (price moves against you)\n", + "# We approximate both as a percentage of the trade value.\n", + "# 两者合并近似为交易金额的固定比例。\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d6b6a55", + "metadata": {}, + "outputs": [], + "source": [ + "class VectorizedBacktester:\n", + " \"\"\"\n", + " A simple vectorized backtesting engine.\n", + " 简单的向量化回测引擎。\n", + "\n", + " Assumptions / 假设:\n", + " • Long-only or long/short positions\n", + " • Trade at next-day's open (用下一天开盘价成交) — conservative assumption\n", + " We approximate this by using the same day's close shifted by 1 day.\n", + " • Round-trip cost (单次交易成本) = 2 × cost_per_trade\n", + " (pay cost on entry AND exit / 进出各收一次)\n", + " • No leverage (无杠杆), position size is 100% of capital when in trade\n", + " \"\"\"\n", + "\n", + " def __init__(\n", + " self,\n", + " prices: pd.Series,\n", + " signal: pd.Series,\n", + " cost_per_trade: float = 0.001, # 0.1% one-way / 单向 0.1% (含佣金+滑点)\n", + " initial_capital: float = 1_000_000.0, # 初始资金 / initial capital\n", + " name: str = \"Strategy\",\n", + " ):\n", + " self.prices = prices\n", + " self.signal = signal.reindex(prices.index).fillna(0)\n", + " self.cost_per_trade = cost_per_trade\n", + " self.initial_capital = initial_capital\n", + " self.name = name\n", + " self._run()\n", + "\n", + " def _run(self):\n", + " \"\"\"Core backtesting logic. 核心回测逻辑。\"\"\"\n", + " prices = self.prices\n", + " signal = self.signal\n", + "\n", + " # ── Daily price return 日收益率 ────────────────────────────────────\n", + " daily_ret = prices.pct_change().fillna(0)\n", + "\n", + " # ── Strategy return (before costs) 策略日收益率(扣除成本前)─────────\n", + " # Strategy return = signal × market return\n", + " # 策略当日收益率 = 持仓方向 × 市场当日收益率\n", + " strat_ret_gross = signal * daily_ret\n", + "\n", + " # ── Transaction cost 交易成本 ──────────────────────────────────────\n", + " # Detect position changes (signal changes from one day to the next)\n", + " # 检测仓位变化(信号从一天到下一天发生变化)\n", + " position_change = signal.diff().fillna(0).abs() # >0 means we traded\n", + " # Cost is charged each time position changes\n", + " # 每次仓位变化时扣除成本\n", + " cost = position_change * self.cost_per_trade\n", + "\n", + " # ── Net strategy return 策略净收益率 ───────────────────────────────\n", + " strat_ret_net = strat_ret_gross - cost\n", + "\n", + " # ── Equity curve 净值曲线 ───────────────────────────────────────────\n", + " # The equity curve tracks how 1 unit of capital grows over time.\n", + " # 净值曲线追踪单位资本随时间的增长。\n", + " # (1 + daily_net_return) compounded every day\n", + " equity = self.initial_capital * (1 + strat_ret_net).cumprod()\n", + " equity_bh = self.initial_capital * (1 + daily_ret).cumprod() # Buy & Hold benchmark\n", + "\n", + " # ── Drawdown 回撤 ──────────────────────────────────────────────────\n", + " # Drawdown measures how far we are from the peak at any point in time.\n", + " # 回撤衡量当前净值距离历史最高点的跌幅。\n", + " rolling_max = equity.cummax()\n", + " drawdown = (equity - rolling_max) / rolling_max # always <= 0\n", + "\n", + " # Store results for later analysis\n", + " self.daily_ret = daily_ret\n", + " self.strat_ret = strat_ret_net\n", + " self.equity = equity\n", + " self.equity_bh = equity_bh\n", + " self.drawdown = drawdown\n", + " self.n_trades = int((position_change > 0).sum())\n", + " self.total_cost = cost.sum()\n", + "\n", + " # ── Performance metrics 绩效指标 ──────────────────────────────────────────\n", + " #\n", + " # A well-rounded strategy evaluation uses multiple metrics, because\n", + " # no single number captures the full picture.\n", + " # 全面的策略评估需要多个指标,因为单一数字无法描述全貌。\n", + " #\n", + " # Key metrics / 关键指标:\n", + " # Total Return 总收益率 — how much did we make in total?\n", + " # CAGR 年化复合增长率 — annualized compounded growth rate\n", + " # Sharpe Ratio 夏普比率 — return per unit of total risk (risk-adjusted)\n", + " # Sortino Ratio 索提诺比率 — return per unit of DOWNSIDE risk only\n", + " # Max Drawdown 最大回撤 — worst peak-to-trough decline\n", + " # Calmar Ratio 卡玛比率 — CAGR / Max Drawdown (reward vs worst loss)\n", + " # Win Rate 胜率 — fraction of days (or trades) with positive P&L\n", + " # Profit Factor 盈亏比 — total profit / total loss\n", + "\n", + " def metrics(self) -> dict:\n", + " \"\"\"Compute and return a dictionary of performance metrics.\n", + " 计算并返回绩效指标字典。\"\"\"\n", + " r = self.strat_ret\n", + " eq = self.equity\n", + " n = len(r)\n", + " years = n / 252.0 # approximate years in sample / 样本年数估算\n", + "\n", + " # Total return / 总收益率\n", + " total_return = (eq.iloc[-1] / self.initial_capital) - 1\n", + "\n", + " # CAGR 年化复合增长率\n", + " # CAGR = (EndValue / StartValue)^(1/years) - 1\n", + " cagr = (1 + total_return) ** (1 / years) - 1\n", + "\n", + " # Annualized volatility 年化波动率\n", + " ann_vol = r.std() * np.sqrt(252)\n", + "\n", + " # Sharpe Ratio 夏普比率\n", + " # Sharpe = (Mean excess return) / StdDev(return) × √252\n", + " # Excess return = strategy return - risk-free rate\n", + " # 超额收益率 = 策略收益率 - 无风险利率\n", + " # We use 0 as risk-free rate for simplicity (or assume it's netted out)\n", + " risk_free = 0.0\n", + " sharpe = (r.mean() - risk_free / 252) / r.std() * np.sqrt(252) if r.std() > 0 else 0\n", + "\n", + " # Sortino Ratio 索提诺比率\n", + " # Like Sharpe but only penalizes DOWNSIDE volatility\n", + " # 类似夏普,但只惩罚下行波动率(亏损波动率)\n", + " downside = r[r < 0]\n", + " downside_std = downside.std() * np.sqrt(252) if len(downside) > 0 else 1e-9\n", + " sortino = (cagr - risk_free) / downside_std if downside_std > 0 else 0\n", + "\n", + " # Maximum Drawdown 最大回撤\n", + " max_dd = self.drawdown.min() # most negative value (最大负值)\n", + "\n", + " # Calmar Ratio 卡玛比率\n", + " # Calmar = CAGR / |Max Drawdown|\n", + " calmar = cagr / abs(max_dd) if max_dd != 0 else 0\n", + "\n", + " # Win rate 胜率 (fraction of trading days with positive return)\n", + " win_rate = (r > 0).mean()\n", + "\n", + " # Profit factor 盈亏比\n", + " # = Sum of positive returns / |Sum of negative returns|\n", + " gross_profit = r[r > 0].sum()\n", + " gross_loss = abs(r[r < 0].sum())\n", + " profit_factor = gross_profit / gross_loss if gross_loss > 0 else np.inf\n", + "\n", + " return {\n", + " \"总收益率 Total Return\": f\"{total_return:.2%}\",\n", + " \"年化收益率 CAGR\": f\"{cagr:.2%}\",\n", + " \"年化波动率 Ann. Volatility\": f\"{ann_vol:.2%}\",\n", + " \"夏普比率 Sharpe Ratio\": f\"{sharpe:.3f}\",\n", + " \"索提诺比率 Sortino Ratio\": f\"{sortino:.3f}\",\n", + " \"最大回撤 Max Drawdown\": f\"{max_dd:.2%}\",\n", + " \"卡玛比率 Calmar Ratio\": f\"{calmar:.3f}\",\n", + " \"胜率 Win Rate\": f\"{win_rate:.2%}\",\n", + " \"盈亏比 Profit Factor\": f\"{profit_factor:.3f}\",\n", + " \"交易次数 # Trades\": str(self.n_trades),\n", + " \"总成本 Total Cost\": f\"{self.total_cost:.4%}\",\n", + " }\n", + "\n", + " def print_metrics(self):\n", + " \"\"\"Pretty-print the performance report. 格式化打印绩效报告。\"\"\"\n", + " print(f\"\\n{'=' * 55}\")\n", + " print(f\" 策略绩效报告 / Performance Report: {self.name}\")\n", + " print(f\"{'=' * 55}\")\n", + " for k, v in self.metrics().items():\n", + " print(f\" {k:<35} {v}\")\n", + " print(f\"{'=' * 55}\")" + ] + }, + { + "cell_type": "markdown", + "id": "ea411c63", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 5: Run Backtests 执行回测\n", + "\n", + "# =============================================================================\n", + "\n", + "# ── Strategy A: MA Crossover 双均线策略 ──────────────────────────────────────" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e13a487c", + "metadata": {}, + "outputs": [], + "source": [ + "bt_ma = VectorizedBacktester(\n", + " prices=price,\n", + " signal=ma_signal, # +1 = long, 0 = flat\n", + " cost_per_trade=0.001, # 0.1% per trade (reasonable for liquid stocks)\n", + " name=\"双均线策略 (MA Crossover 20/60)\",\n", + ")\n", + "\n", + "# ── Strategy B: RSI Mean Reversion RSI均值回归策略 ──────────────────────────\n", + "bt_rsi = VectorizedBacktester(\n", + " prices=price,\n", + " signal=rsi_signal_shifted, # +1 = long, -1 = short, 0 = flat\n", + " cost_per_trade=0.001,\n", + " name=\"RSI均值回归策略 (RSI Mean Reversion 14)\",\n", + ")\n", + "\n", + "# ── Benchmark: Buy & Hold 基准:买入并持有 ───────────────────────────────────\n", + "# Buy & Hold (买入持有) is always our benchmark: simply hold the asset forever.\n", + "# It requires zero skill and zero effort — any strategy must beat this to\n", + "# justify the extra complexity and transaction costs.\n", + "# 买入持有是永远的基准策略:无需技能、零成本。任何策略都必须超越它才有意义。\n", + "bt_bh = VectorizedBacktester(\n", + " prices=price,\n", + " signal=pd.Series(1, index=price.index, dtype=float), # always long / 始终做多\n", + " cost_per_trade=0.0, # no trading costs / 无交易成本\n", + " name=\"Buy & Hold 基准 (买入持有)\",\n", + ")\n", + "\n", + "bt_ma.print_metrics()\n", + "bt_rsi.print_metrics()\n", + "bt_bh.print_metrics()" + ] + }, + { + "cell_type": "markdown", + "id": "9d601da1", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 6: Visualization 可视化\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6b6bfc8", + "metadata": {}, + "outputs": [], + "source": [ + "fig = plt.figure(figsize=(16, 22))\n", + "gs = gridspec.GridSpec(6, 2, figure=fig, hspace=0.45, wspace=0.3)\n", + "\n", + "# ── Plot 1: Price + MA signals 价格 + 均线信号 ────────────────────────────────\n", + "ax1 = fig.add_subplot(gs[0, :]) # span full width\n", + "ax1.plot(price, color=\"#1f77b4\", linewidth=1, label=\"价格 Price\")\n", + "ax1.plot(ma_short, color=\"orange\", linewidth=1.2, label=f\"SMA{SHORT_WIN} (快线)\")\n", + "ax1.plot(ma_long, color=\"red\", linewidth=1.2, label=f\"SMA{LONG_WIN} (慢线)\")\n", + "\n", + "# Shade long periods (持多仓的区间着色)\n", + "ax1.fill_between(\n", + " price.index, price.min(), price.max(),\n", + " where=(ma_signal == 1).values,\n", + " alpha=0.12, color=\"green\", label=\"多头持仓区间 Long Period\"\n", + ")\n", + "ax1.set_title(\"策略A — 双均线信号 (MA Crossover Signals)\", fontsize=13, fontweight=\"bold\")\n", + "ax1.legend(loc=\"upper left\", fontsize=8)\n", + "ax1.set_ylabel(\"价格 Price\")\n", + "ax1.grid(alpha=0.3)\n", + "\n", + "# ── Plot 2: RSI RSI指标 ────────────────────────────────────────────────────\n", + "ax2 = fig.add_subplot(gs[1, :])\n", + "ax2.plot(rsi14, color=\"purple\", linewidth=1)\n", + "ax2.axhline(RSI_OVERBOUGHT, color=\"red\", linestyle=\"--\", linewidth=1, label=f\"超买线 {RSI_OVERBOUGHT}\")\n", + "ax2.axhline(RSI_OVERSOLD, color=\"green\", linestyle=\"--\", linewidth=1, label=f\"超卖线 {RSI_OVERSOLD}\")\n", + "ax2.axhline(50, color=\"gray\", linestyle=\":\", linewidth=0.8)\n", + "ax2.fill_between(rsi14.index, RSI_OVERSOLD, rsi14,\n", + " where=(rsi14 < RSI_OVERSOLD), alpha=0.25, color=\"green\",\n", + " label=\"超卖区域 Oversold\")\n", + "ax2.fill_between(rsi14.index, rsi14, RSI_OVERBOUGHT,\n", + " where=(rsi14 > RSI_OVERBOUGHT), alpha=0.25, color=\"red\",\n", + " label=\"超买区域 Overbought\")\n", + "ax2.set_ylim(0, 100)\n", + "ax2.set_title(f\"策略B指标 — RSI({14}) 均值回归信号 (RSI Mean Reversion)\", fontsize=13, fontweight=\"bold\")\n", + "ax2.set_ylabel(\"RSI\")\n", + "ax2.legend(loc=\"upper left\", fontsize=8, ncol=2)\n", + "ax2.grid(alpha=0.3)\n", + "\n", + "# ── Plot 3: Bollinger Bands 布林带 ────────────────────────────────────────────\n", + "ax3 = fig.add_subplot(gs[2, :])\n", + "ax3.plot(price, color=\"#1f77b4\", linewidth=1, label=\"价格 Price\")\n", + "ax3.plot(bb[\"mid\"], color=\"orange\", linewidth=1.2, label=\"中轨 Middle (SMA20)\")\n", + "ax3.plot(bb[\"upper\"],color=\"red\", linewidth=1, linestyle=\"--\", label=\"上轨 Upper (+2σ)\")\n", + "ax3.plot(bb[\"lower\"],color=\"green\", linewidth=1, linestyle=\"--\", label=\"下轨 Lower (-2σ)\")\n", + "ax3.fill_between(price.index, bb[\"upper\"], bb[\"lower\"], alpha=0.07, color=\"blue\")\n", + "ax3.set_title(\"布林带 (Bollinger Bands 20, 2σ)\", fontsize=13, fontweight=\"bold\")\n", + "ax3.set_ylabel(\"价格 Price\")\n", + "ax3.legend(loc=\"upper left\", fontsize=8, ncol=2)\n", + "ax3.grid(alpha=0.3)\n", + "\n", + "# ── Plot 4: MACD MACD指标 ────────────────────────────────────────────────────\n", + "ax4 = fig.add_subplot(gs[3, :])\n", + "ax4.plot(macd_df[\"macd\"], color=\"blue\", linewidth=1, label=\"MACD线 (DIF)\")\n", + "ax4.plot(macd_df[\"signal\"], color=\"orange\", linewidth=1, label=\"信号线 (DEA)\")\n", + "colors = [\"green\" if v >= 0 else \"red\" for v in macd_df[\"histogram\"]]\n", + "ax4.bar(macd_df.index, macd_df[\"histogram\"], color=colors, alpha=0.5, width=1, label=\"柱状图 Histogram\")\n", + "ax4.axhline(0, color=\"black\", linewidth=0.8)\n", + "ax4.set_title(\"MACD (12/26/9) — 趋势确认指标 (Trend Confirmation)\", fontsize=13, fontweight=\"bold\")\n", + "ax4.set_ylabel(\"MACD\")\n", + "ax4.legend(loc=\"upper left\", fontsize=8)\n", + "ax4.grid(alpha=0.3)\n", + "\n", + "# ── Plot 5: Equity Curves 净值曲线 ────────────────────────────────────────────\n", + "ax5 = fig.add_subplot(gs[4, :])\n", + "ax5.plot(bt_ma.equity, color=\"blue\", linewidth=1.5, label=\"策略A: 双均线 MA Crossover\")\n", + "ax5.plot(bt_rsi.equity, color=\"purple\", linewidth=1.5, label=\"策略B: RSI 均值回归 RSI Reversion\")\n", + "ax5.plot(bt_bh.equity, color=\"gray\", linewidth=1.2, linestyle=\"--\", label=\"基准: 买入持有 Buy & Hold\")\n", + "ax5.set_title(\"净值曲线对比 (Equity Curve Comparison)\", fontsize=13, fontweight=\"bold\")\n", + "ax5.set_ylabel(\"账户价值 Portfolio Value (元)\")\n", + "ax5.legend(loc=\"upper left\", fontsize=9)\n", + "ax5.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"¥{x/1e4:.0f}万\"))\n", + "ax5.grid(alpha=0.3)\n", + "\n", + "# ── Plot 6: Drawdown 回撤曲线 ────────────────────────────────────────────────\n", + "ax6 = fig.add_subplot(gs[5, :])\n", + "ax6.fill_between(bt_ma.drawdown.index, bt_ma.drawdown, 0, alpha=0.5, color=\"blue\", label=\"策略A\")\n", + "ax6.fill_between(bt_rsi.drawdown.index, bt_rsi.drawdown, 0, alpha=0.5, color=\"purple\", label=\"策略B\")\n", + "ax6.fill_between(bt_bh.drawdown.index, bt_bh.drawdown, 0, alpha=0.3, color=\"gray\", label=\"Buy & Hold\")\n", + "ax6.set_title(\"回撤曲线 (Drawdown Curves)\", fontsize=13, fontweight=\"bold\")\n", + "ax6.set_ylabel(\"回撤幅度 Drawdown\")\n", + "ax6.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f\"{x:.0%}\"))\n", + "ax6.legend(loc=\"lower left\", fontsize=9)\n", + "ax6.grid(alpha=0.3)\n", + "\n", + "plt.suptitle(\n", + " \"量化交易策略开发与回测演示\\nQuantitative Trading: Strategy Development & Backtesting\",\n", + " fontsize=15, fontweight=\"bold\", y=1.005,\n", + ")\n", + "plt.savefig(\"strategy_backtest_demo.png\", dpi=120, bbox_inches=\"tight\")\n", + "plt.show()\n", + "print(\"\\n[图表] 已保存至 strategy_backtest_demo.png\")" + ] + }, + { + "cell_type": "markdown", + "id": "ff8384a5", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 7: Walk-Forward Validation 滚动前向验证\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# A critical warning for all new quant traders / 对所有量化新手的重要警告:\n", + "#\n", + "# In-sample overfitting (样本内过拟合) is the #1 trap in backtesting.\n", + "# 样本内过拟合是回测中最大的陷阱。\n", + "#\n", + "# If you test 100 different parameter sets on the same data and pick the best,\n", + "# that \"best\" result will almost certainly NOT hold out of sample.\n", + "# 如果在同一份数据上测试100组参数并选最好的,这个\"最优\"结果在样本外几乎必然失效。\n", + "# This is called data snooping bias / 数据窥探偏差 or p-hacking.\n", + "#\n", + "# Walk-Forward Validation (滚动前向验证) helps guard against this:\n", + "# ┌──────────────────────────────────────────────────────────────────────┐\n", + "# │ Window 1: [TRAIN period 1] → optimize params → TEST on period 1+ │\n", + "# │ Window 2: [TRAIN period 2] → optimize params → TEST on period 2+ │\n", + "# │ …repeat, always training on past, testing on future │\n", + "# │ 始终用过去数据训练,用未来数据测试 │\n", + "# └──────────────────────────────────────────────────────────────────────┘\n", + "# Only report the concatenated OUT-OF-SAMPLE test results.\n", + "# 只汇报样本外(OOS)的测试结果。\n", + "#\n", + "# Below: a simplified version — we just split into train / test (80/20).\n", + "# 下面是简化版:直接按 80/20 切分训练集和测试集。\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e99677e", + "metadata": {}, + "outputs": [], + "source": [ + "TRAIN_RATIO = 0.8\n", + "split_idx = int(len(price) * TRAIN_RATIO)\n", + "split_date = price.index[split_idx]\n", + "\n", + "price_train = price.iloc[:split_idx]\n", + "price_test = price.iloc[split_idx:]\n", + "\n", + "print(f\"\\n{'=' * 55}\")\n", + "print(f\" 滚动前向验证 / Walk-Forward Split\")\n", + "print(f\"{'=' * 55}\")\n", + "print(f\" 训练期 Train: {price_train.index[0].date()} → {price_train.index[-1].date()} ({len(price_train)} 天)\")\n", + "print(f\" 测试期 Test : {price_test.index[0].date()} → {price_test.index[-1].date()} ({len(price_test)} 天)\")\n", + "\n", + "# ── Optimize MA windows on TRAIN set 在训练集上优化均线参数 ────────────────────\n", + "#\n", + "# Grid search (网格搜索): try all combinations in the parameter space.\n", + "# This is the simplest optimization method — good for small parameter spaces.\n", + "# 网格搜索:遍历参数空间内的所有组合。适合参数空间小的情形。\n", + "\n", + "print(\"\\n[优化] 在训练集上搜索最优均线参数...\")\n", + "print(\" (搜索空间: short=[5,10,15,20,30], long=[30,40,50,60,80,100])\")\n", + "\n", + "best_sharpe = -np.inf\n", + "best_short = SHORT_WIN\n", + "best_long = LONG_WIN\n", + "results_grid = []\n", + "\n", + "for sw in [5, 10, 15, 20, 30]:\n", + " for lw in [30, 40, 50, 60, 80, 100]:\n", + " if sw >= lw:\n", + " continue # short must be shorter than long / 短期必须小于长期\n", + " ma_s = sma(price_train, sw)\n", + " ma_l = sma(price_train, lw)\n", + " sig = (ma_s > ma_l).astype(int).shift(1).fillna(0)\n", + " bt = VectorizedBacktester(price_train, sig, cost_per_trade=0.001, name=\"grid\")\n", + " m = bt.metrics()\n", + " sharpe_val = float(m[\"夏普比率 Sharpe Ratio\"])\n", + " results_grid.append({\"short\": sw, \"long\": lw, \"sharpe\": sharpe_val})\n", + " if sharpe_val > best_sharpe:\n", + " best_sharpe = sharpe_val\n", + " best_short = sw\n", + " best_long = lw\n", + "\n", + "print(f\"\\n 最优参数 (训练集 in-sample): short={best_short}, long={best_long}\")\n", + "print(f\" 训练集夏普比率 In-sample Sharpe: {best_sharpe:.3f}\")\n", + "\n", + "# ── Apply best params on TEST set 将最优参数应用于测试集 ──────────────────────\n", + "ma_s_test = sma(price_test, best_short)\n", + "ma_l_test = sma(price_test, best_long)\n", + "sig_test = (ma_s_test > ma_l_test).astype(int).shift(1).fillna(0)\n", + "\n", + "bt_test = VectorizedBacktester(price_test, sig_test, cost_per_trade=0.001,\n", + " name=f\"MA({best_short}/{best_long}) — 测试集 OOS\")\n", + "bt_test.print_metrics()\n", + "\n", + "print(\"\\n\" + \"=\" * 55)\n", + "print(\" ⚠️ 注意 / WARNING:\")\n", + "print(\" 训练集(in-sample)夏普 通常高于 测试集(out-of-sample)夏普\")\n", + "print(\" In-sample Sharpe is typically HIGHER than out-of-sample.\")\n", + "print(\" 夏普衰减 (Sharpe decay) 是策略过拟合的典型信号。\")\n", + "print(\" Sharpe decay is a classic sign of overfitting.\")\n", + "print(\"=\" * 55)" + ] + }, + { + "cell_type": "markdown", + "id": "95c33a90", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 8: Return Distribution Analysis 收益率分布分析\n", + "\n", + "# -----------------------------------------------------------------------------\n", + "# Before trusting your Sharpe ratio, check if the return distribution\n", + "# violates the normality assumption.\n", + "# 在相信夏普比率之前,检验收益率分布是否违背正态假设。\n", + "#\n", + "# Real returns typically show:\n", + "# 真实收益率通常呈现:\n", + "# Fat tails (厚尾 / leptokurtosis): extreme events more frequent than normal\n", + "# Negative skew (负偏态): crashes are larger than rallies\n", + "#\n", + "# A high Sharpe ratio on a fat-tailed distribution can be misleading.\n", + "# 厚尾分布下的高夏普比率可能具有误导性。\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e74d07b8", + "metadata": {}, + "outputs": [], + "source": [ + "fig2, axes = plt.subplots(1, 2, figsize=(14, 5))\n", + "\n", + "# ── Return distribution histogram 收益率直方图 ─────────────────────────────\n", + "ax = axes[0]\n", + "r_ma = bt_ma.strat_ret.dropna()\n", + "r_bh = bt_bh.strat_ret.dropna()\n", + "\n", + "ax.hist(r_bh, bins=80, alpha=0.5, color=\"gray\", density=True, label=\"Buy & Hold\")\n", + "ax.hist(r_ma, bins=80, alpha=0.5, color=\"blue\", density=True, label=\"策略A: MA Crossover\")\n", + "\n", + "# Overlay a normal distribution for comparison / 叠加正态分布对比\n", + "x_range = np.linspace(r_bh.min(), r_bh.max(), 300)\n", + "ax.plot(x_range, stats.norm.pdf(x_range, r_bh.mean(), r_bh.std()),\n", + " color=\"red\", linewidth=1.5, linestyle=\"--\", label=\"正态分布 Normal Dist.\")\n", + "ax.set_title(\"收益率分布 (Return Distribution)\", fontsize=12)\n", + "ax.set_xlabel(\"日收益率 Daily Return\")\n", + "ax.set_ylabel(\"频率密度 Density\")\n", + "ax.legend(fontsize=8)\n", + "ax.grid(alpha=0.3)\n", + "\n", + "# Print distribution stats\n", + "print(f\"\\n[分布] 策略A — 日收益率统计:\")\n", + "print(f\" 偏度 Skewness : {r_ma.skew():.3f} (负值=左尾更厚 fat left tail)\")\n", + "print(f\" 峰度 Kurtosis : {r_ma.kurtosis():.3f} (>0 表示厚尾 fat tails vs normal)\")\n", + "\n", + "# ── Monthly returns heatmap 月度收益热力图 ─────────────────────────────────\n", + "ax = axes[1]\n", + "monthly = bt_ma.strat_ret.resample(\"M\").apply(lambda x: (1 + x).prod() - 1)\n", + "monthly_df = pd.DataFrame({\n", + " \"year\": monthly.index.year,\n", + " \"month\": monthly.index.month,\n", + " \"ret\": monthly.values,\n", + "})\n", + "pivot = monthly_df.pivot(index=\"year\", columns=\"month\", values=\"ret\")\n", + "pivot.columns = [\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"]\n", + "\n", + "import matplotlib.colors as mcolors\n", + "cmap = mcolors.LinearSegmentedColormap.from_list(\"rg\", [\"#d73027\",\"#ffffff\",\"#1a9850\"])\n", + "im = ax.imshow(pivot.values, cmap=cmap, aspect=\"auto\",\n", + " vmin=-0.15, vmax=0.15)\n", + "ax.set_xticks(range(12))\n", + "ax.set_xticklabels(pivot.columns, fontsize=8)\n", + "ax.set_yticks(range(len(pivot.index)))\n", + "ax.set_yticklabels(pivot.index, fontsize=9)\n", + "for i in range(len(pivot.index)):\n", + " for j in range(12):\n", + " v = pivot.values[i, j]\n", + " if not np.isnan(v):\n", + " ax.text(j, i, f\"{v:.1%}\", ha=\"center\", va=\"center\", fontsize=6,\n", + " color=\"black\" if abs(v) < 0.08 else \"white\")\n", + "ax.set_title(\"策略A月度收益热力图\\n(Monthly Return Heatmap)\", fontsize=11)\n", + "plt.colorbar(im, ax=ax, format=plt.FuncFormatter(lambda x, _: f\"{x:.0%}\"))\n", + "\n", + "plt.tight_layout()\n", + "plt.savefig(\"return_distribution.png\", dpi=120, bbox_inches=\"tight\")\n", + "plt.show()\n", + "print(\"[图表] 已保存至 return_distribution.png\")" + ] + }, + { + "cell_type": "markdown", + "id": "4dfb5f58", + "metadata": {}, + "source": [ + "# =============================================================================\n", + "# SECTION 9: Summary & Next Steps 总结与后续\n", + "\n", + "# =============================================================================" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bac6b3d", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"\"\"\n", + "{'=' * 70}\n", + " 总结 Summary\n", + "{'=' * 70}\n", + " 本 Demo 演示了量化策略开发与回测的完整流程:\n", + "\n", + " ① 技术指标计算 Technical Indicators\n", + " SMA / EMA / RSI / MACD / Bollinger Bands\n", + "\n", + " ② 信号生成 Signal Generation\n", + " 策略A: 双均线金叉死叉 (MA Crossover) — 趋势跟随\n", + " 策略B: RSI 超买超卖 (RSI Reversion) — 均值回归\n", + "\n", + " ③ 向量化回测引擎 Vectorized Backtester\n", + " 考虑了交易成本(佣金+滑点)和前视偏差(lookahead bias)\n", + "\n", + " ④ 绩效指标 Performance Metrics\n", + " Sharpe / Sortino / Max Drawdown / Calmar / Win Rate / Profit Factor\n", + "\n", + " ⑤ 前向验证 Walk-Forward Validation\n", + " 训练集优化参数 → 测试集验证 → 防止过拟合\n", + "\n", + " ⑥ 收益率分布 Return Distribution\n", + " 偏度/峰度检验,月度热力图\n", + "\n", + " 下一步学习方向 Next Steps:\n", + " ──────────────────────────────────────────────────────────────\n", + " • 因子选股策略 (Alpha Factor Models) — Fama-French, Momentum\n", + " • 组合优化 (Portfolio Optimization) — Mean-Variance, Risk Parity\n", + " • 事件驱动回测 (Event-Driven Backtesting) — more realistic execution\n", + " • 机器学习信号 (ML-based Signals) — XGBoost, LSTM for return prediction\n", + " • 风险管理 (Risk Management) — Position sizing, Stop-loss, VaR\n", + "{'=' * 70}\n", + "\"\"\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (trading)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}