{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "4fa4cac8",
   "metadata": {},
   "source": [
    "# 量化交易工作坊 · 任务三：策略首秀——双均线策略\n",
    "\n",
    "**标的：芯动联科（688582.SH）**\n",
    "\n",
    "本 Notebook 完成以下工作：\n",
    "1. 解释双均线策略中的**金叉 / 死叉**概念；\n",
    "2. 解释量化策略评价的基础指标：最大回撤（MDD）、夏普比率（Sharpe Ratio）、累计回报（Cumulative Return）、胜率、盈亏比、年化收益；\n",
    "3. Python 实现：加载股价数据 → 计算均线 → 生成交易信号 → 可视化（股价/均线/买卖点/止损）→ 模拟交易与回测（含交易成本与硬止损）→ 计算指标；\n",
    "4. 切换不同股票与均线周期，观察收益变化并总结策略适用场景。\n",
    "\n",
    "> 说明：本 Notebook 复用 `app/strategy_lib.py` 中的策略引擎（含交易成本与硬止损建模），与 Web 系统、PDF 报告保持完全一致的实现。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5bb68fc8",
   "metadata": {},
   "source": [
    "## 一、双均线策略原理\n",
    "\n",
    "双均线策略（Dual Moving Average）是最经典的趋势跟踪策略之一。它同时计算两条不同周期的移动平均线：\n",
    "\n",
    "- **短均线（快线）**：如 5 日收盘价均值，对价格变化更敏感；\n",
    "- **长均线（慢线）**：如 15 日收盘价均值，反映中长期趋势，更加平滑。\n",
    "\n",
    "移动平均线本身具有**滞后性**——周期越长，滞后越明显。当短均线从下方穿越长均线时，意味着短期动能强于长期趋势，市场可能由跌转涨；反之则由涨转跌。策略据此产生两类核心信号：\n",
    "\n",
    "### 1. 金叉（Golden Cross）—— 买入信号\n",
    "**短均线由下向上穿越长均线**。它代表短期价格走强、趋势可能反转向上，是经典的入场（做多）信号。\n",
    "\n",
    "### 2. 死叉（Death Cross）—— 卖出信号\n",
    "**短均线由上向下穿越长均线**。它代表短期价格走弱、趋势可能反转向下，是经典的离场（平仓）信号。\n",
    "\n",
    "### 策略逻辑\n",
    "- 出现**金叉** → 买入并持有（position = 1）；\n",
    "- 出现**死叉** 或 **触及硬止损** → 卖出清仓（position = 0）；\n",
    "- 其余时间维持当前持仓状态。\n",
    "\n",
    "双均线策略的本质是\"**用均线的相对位置刻画趋势方向，用交叉事件捕捉趋势拐点**\"，从而把连续的价格波动转化为离散的买卖决策。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "908baa45",
   "metadata": {},
   "source": [
    "## 二、量化策略效果的基础评价指标\n",
    "\n",
    "仅有收益数字不足以评价策略，必须结合风险与稳定性。本任务关注以下核心指标：\n",
    "\n",
    "### 1. 累计回报（Cumulative Return）\n",
    "策略在整个回测区间内的总收益率：$R_{cum} = E_{end}/E_{start} - 1$。衡量\"最终赚了多少\"，但不反映过程波动。\n",
    "\n",
    "### 2. 最大回撤（Maximum Drawdown, MDD）\n",
    "净值从历史高点回落到随后最低点的最大跌幅：$MDD = \\min_t (E_t / \\max_{s\\le t}E_s - 1)$。刻画最坏情景下的浮亏深度，越接近 0 越好。\n",
    "\n",
    "### 3. 夏普比率（Sharpe Ratio）\n",
    "单位总风险换来的超额收益：$Sharpe = \\bar r / \\sigma_r \\times \\sqrt{N}$（$N$ 取 252）。越高说明同等波动下回报越优。\n",
    "\n",
    "### 4. 年化收益（Annualized Return）\n",
    "把区间总收益折算到年度：$(E_{end}/E_{start})^{252/n} - 1$，便于跨周期比较。\n",
    "\n",
    "### 5. 胜率（Win Rate）与盈亏比（Profit/Loss Ratio）\n",
    "- **胜率** = 盈利交易数 / 总交易数；\n",
    "- **盈亏比** = 平均盈利 / 平均亏损。\n",
    "二者刻画策略\"以小博大\"的能力：盈亏比高说明单笔盈利足以覆盖多笔小额亏损（趋势策略的典型特征）。\n",
    "\n",
    "> 本 Notebook 同时给出\"买入持有\"基准，并**在回测中扣除交易成本（佣金+印花税+滑点）与设置硬止损**，以更贴近真实交易。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a4552417",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 0. 环境与策略引擎\n",
    "import os\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "plt.rcParams['figure.dpi'] = 110\n",
    "plt.rcParams['axes.unicode_minus'] = False\n",
    "\n",
    "# 复用 app/strategy_lib.py 引擎（自动完成中文字体与绘图设置）\n",
    "sys_path = os.path.abspath(os.path.join(os.getcwd(), \"..\", \"app\"))\n",
    "if os.path.isdir(sys_path):\n",
    "    import sys\n",
    "    sys.path.insert(0, sys_path)\n",
    "from strategy_lib import (load_data, compute_ma, generate_signals, backtest,\n",
    "                          make_figure, make_equity_figure, make_drawdown_figure,\n",
    "                          risk_return_points, make_risk_return_figure)\n",
    "\n",
    "def _find_root():\n",
    "    d = os.getcwd()\n",
    "    for _ in range(6):\n",
    "        if os.path.exists(os.path.join(d, \"data\", \"688582.SH.csv\")):\n",
    "            return d\n",
    "        parent = os.path.dirname(d)\n",
    "        if parent == d:\n",
    "            break\n",
    "        d = parent\n",
    "    return os.getcwd()\n",
    "\n",
    "ROOT = _find_root()\n",
    "DATA = os.path.join(ROOT, \"data\", \"688582.SH.csv\")\n",
    "print(\"数据文件:\", DATA, \"存在:\", os.path.exists(DATA))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5d594633",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 3.1 加载已存储的股价数据\n",
    "df = load_data(DATA)\n",
    "print(\"字段:\", list(df.columns))\n",
    "print(\"样本数:\", len(df), \"| 区间:\", df['trade_date'].iloc[0], \"->\", df['trade_date'].iloc[-1])\n",
    "df[[\"trade_date\", \"open\", \"high\", \"low\", \"close\", \"vol\"]].head()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cc16623b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 3.2 设定短/长均线周期，计算均线数据\n",
    "SHORT, LONG = 5, 15\n",
    "\n",
    "def compute_ma(df, short, long):\n",
    "    df = df.copy()\n",
    "    df[f\"ma{short}\"] = df[\"close\"].rolling(short).mean()\n",
    "    df[f\"ma{long}\"] = df[\"close\"].rolling(long).mean()\n",
    "    return df\n",
    "\n",
    "df = compute_ma(df, SHORT, LONG)\n",
    "df[[\"trade_date\", \"close\", f\"ma{SHORT}\", f\"ma{LONG}\"]].tail()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0fed99d4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 3.3 计算买入/卖出交易信号（金叉 / 死叉）\n",
    "def generate_signals(df, short, long):\n",
    "    df = df.copy()\n",
    "    s, l = f\"ma{short}\", f\"ma{long}\"\n",
    "    df[\"position\"] = (df[s] > df[l]).astype(int)   # 1=持仓, 0=空仓\n",
    "    df[\"signal\"] = df[\"position\"].diff().fillna(0) # 持仓跳变处\n",
    "    df[\"buy\"]  = (df[\"signal\"] ==  1)              # 金叉 -> 买入\n",
    "    df[\"sell\"] = (df[\"signal\"] == -1)              # 死叉 -> 卖出\n",
    "    return df\n",
    "\n",
    "df = generate_signals(df, SHORT, LONG)\n",
    "n_buy  = int(df[\"buy\"].sum())\n",
    "n_sell = int(df[\"sell\"].sum())\n",
    "print(f\"金叉(买入)次数: {n_buy} | 死叉(卖出)次数: {n_sell}\")\n",
    "df[[\"trade_date\", \"close\", f\"ma{SHORT}\", f\"ma{LONG}\", \"buy\", \"sell\"]].tail(8)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b95f506c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 3.4 模拟交易回测（含交易成本与硬止损），计算全部量化指标\n",
    "# 成本与止损与 Web 系统、报告一致：佣金0.03%、印花税0.05%、滑点0.05%、硬止损5%\n",
    "res = backtest(df, SHORT, LONG,\n",
    "               commission=0.0003, stamp_tax=0.0005, slippage=0.0005, stop_pct=0.05)\n",
    "res_df = res[\"df\"]\n",
    "m = res[\"metrics\"]\n",
    "print(\"========== 双均线策略回测指标（芯动联科，短=5, 长=15）==========\")\n",
    "print(f\"累计回报   : {m['cumulative_return']*100:7.2f}%\")\n",
    "print(f\"基准(买入持有): {m['benchmark_return']*100:7.2f}%\")\n",
    "print(f\"超额收益   : {m['excess_return']*100:7.2f}%\")\n",
    "print(f\"最大回撤MDD : {m['mdd']*100:7.2f}%\")\n",
    "print(f\"夏普比率   : {m['sharpe']:7.2f}\")\n",
    "print(f\"年化收益   : {m['annualized_return']*100:7.2f}%\")\n",
    "print(f\"胜率       : {m['win_rate']*100:7.2f}%\")\n",
    "print(f\"盈亏比     : {m['pl_ratio']:7.2f}\")\n",
    "print(f\"交易次数   : {m['n_trades']}\")\n",
    "print(f\"期末净值   : {m['final_equity']:,.0f} 元\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fc8080d5",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 3.5 图1：股价 + 长短均线 + 买卖信号 + 止损离场标记\n",
    "fig = make_figure(res_df, SHORT, LONG,\n",
    "    title=f\"图1 芯动联科(688582.SH) 双均线策略信号图  短={SHORT} 长={LONG}\")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5682f17a",
   "metadata": {},
   "source": [
    "**图1 解读：** 上图叠加收盘价（蓝）、短均线（橙）、长均线（绿），并标出金叉（红▲，买入）、死叉（深蓝▼，卖出）与止损离场（橙色×）信号点；下图用阶梯线展示持仓信号（1=持仓，0=空仓），即策略实际发出的交易指令。金叉出现时持仓由 0 跳到 1、死叉或触及 5% 硬止损时由 1 回落到 0。在回测区间内共触发 32 次交易，胜率约 34%——看似不高，但因趋势策略\"盈亏比 > 2\"（平均盈利是平均亏损的两倍以上），少数几笔大趋势盈利足以覆盖多次小额止损亏损。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f6de71d2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 3.6 图2：策略净值 vs 买入持有基准\n",
    "fig = make_equity_figure(res_df, SHORT, LONG,\n",
    "    title=f\"图2 芯动联科(688582.SH) 策略净值 vs 买入持有\")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10a5dc7a",
   "metadata": {},
   "source": [
    "**图2 解读：** 红色实线为按双均线信号逐日模拟交易得到的策略净值（已扣成本），蓝色虚线为同期买入持有基准。本区间内策略累计回报约 +12.55%，虽低于买入持有的 +22.5%（超额 −9.97%），但最大回撤收敛到约 −28.7%（硬止损截断了大量深跌）。在趋势明确上行时策略因均线滞后会晚入场、早离场，累计收益可能低于直接持有——这正是趋势跟踪为控制回撤所付出的\"代价\"，而回撤更小意味着实盘心理压力更低。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d7bfc978",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 3.7 图3：净值回撤曲线（风险可视化）\n",
    "fig = make_drawdown_figure(res_df, SHORT, LONG,\n",
    "    drawdown=m[\"drawdown\"], title=f\"图3 芯动联科(688582.SH) 策略净值回撤曲线\")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1574177e",
   "metadata": {},
   "source": [
    "**图3 解读：** 回撤曲线展示策略净值相对历史高点的回落幅度（越低越糟）。双均线策略在趋势反转、反复金叉死叉的震荡段会持续小幅回撤，而每一段\"下蹲\"都被硬止损限制在一定幅度内（本区间最深约 −28.7%）。与图2 配合，可直观评估\"为了多少收益承担了多大回撤风险\"。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "80cb194c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 3.8 图4：多股票风险-收益散点（短=5, 长=15）\n",
    "STOCK_NAMES = {\"688582.SH\":\"芯动联科\",\"600519.SH\":\"贵州茅台\",\"300750.SZ\":\"宁德时代\",\"000001.SZ\":\"平安银行\"}\n",
    "pts = risk_return_points(DATA, SHORT, LONG, names=STOCK_NAMES)\n",
    "fig = make_risk_return_figure(pts, title=\"图4 多股票风险-收益散点（短=5, 长=15）\")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "307f01b3",
   "metadata": {},
   "source": [
    "**图4 解读：** 横轴为最大回撤 MDD（越低越好），纵轴为年化收益（越高越好），气泡大小/颜色代表夏普比率。理想标的位于\"左上角\"（高收益、低回撤、高夏普）。可见不同股票的风险收益属性差异显著——成长弹性高的标的（如宁德时代）年化与夏普更优，而金融蓝筹（如平安银行）波动小但收益也低。这正是多标的对比的意义：同一套参数在不同标的上表现分化，需结合标的特性选择周期。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a5b0c4fb",
   "metadata": {},
   "source": [
    "## 四、不同股票 × 不同均线周期：对比实验与心得\n",
    "\n",
    "下面在**多只股票**、**多组均线周期**上运行同一套双均线策略，汇总累计回报、最大回撤、夏普、胜率、盈亏比与年化收益，观察策略表现的差异，并据此总结适用场景。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fb17580c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 4.1 多股票 × 多周期 对比\n",
    "import glob\n",
    "\n",
    "STOCK_NAMES = {\"688582.SH\":\"芯动联科\",\"600519.SH\":\"贵州茅台\",\"300750.SZ\":\"宁德时代\",\"000001.SZ\":\"平安银行\"}\n",
    "PERIODS = [(5,15), (5,20), (10,30), (10,60)]\n",
    "\n",
    "rows = []\n",
    "files = {os.path.basename(p).replace('.csv',''): p for p in glob.glob(os.path.join(ROOT,'data','*.csv')) if not os.path.basename(p).startswith('_')}\n",
    "for code, path in files.items():\n",
    "    if code not in STOCK_NAMES:\n",
    "        continue\n",
    "    base = load_data(path)\n",
    "    for (sh, lg) in PERIODS:\n",
    "        d = generate_signals(compute_ma(base, sh, lg), sh, lg)\n",
    "        res = backtest(d, sh, lg, commission=0.0003, stamp_tax=0.0005, slippage=0.0005, stop_pct=0.05)\n",
    "        m = res[\"metrics\"]\n",
    "        rows.append(dict(股票=STOCK_NAMES[code], 代码=code, 短=sh, 长=lg,\n",
    "                         累计回报=f\"{m['cumulative_return']*100:.1f}%\",\n",
    "                         基准=f\"{m['benchmark_return']*100:.1f}%\",\n",
    "                         超额=f\"{m['excess_return']*100:.1f}%\",\n",
    "                         MDD=f\"{m['mdd']*100:.1f}%\",\n",
    "                         Sharpe=f\"{m['sharpe']:.2f}\",\n",
    "                         胜率=f\"{m['win_rate']*100:.1f}%\",\n",
    "                         盈亏比=f\"{m['pl_ratio']:.2f}\",\n",
    "                         年化=f\"{m['annualized_return']*100:.1f}%\",\n",
    "                         交易=m['n_trades']))\n",
    "cmp = pd.DataFrame(rows)\n",
    "cmp\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "981daada",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 4.2 将对比结果保存为 CSV，供报告使用；并统计各股票平均表现\n",
    "cmp.to_csv(os.path.join(ROOT, \"data\", \"_comparison.csv\"), index=False, encoding=\"utf-8-sig\")\n",
    "print(\"对比结果已保存，共\", len(cmp), \"组实验\")\n",
    "def to_num(s):\n",
    "    return s.str.replace('%','',regex=False).astype(float)\n",
    "summary = cmp.copy()\n",
    "for col in [\"累计回报\",\"基准\",\"超额\",\"MDD\",\"Sharpe\",\"胜率\",\"盈亏比\",\"年化\"]:\n",
    "    summary[col] = to_num(summary[col])\n",
    "summary = summary.groupby(\"股票\")[[\"累计回报\",\"基准\",\"超额\",\"MDD\",\"Sharpe\",\"胜率\",\"盈亏比\",\"年化\",\"交易\"]].mean().round(2)\n",
    "summary\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3a4d9543",
   "metadata": {},
   "source": [
    "## 五、总结与心得\n",
    "\n",
    "1. **趋势市 vs 震荡市**：双均线策略在**有明确趋势**的标的上表现最佳（如宁德时代，10/30 组合累计 +56.1%、夏普 0.59），金叉后能持续持有享受上涨；在**区间震荡**的标的上（如平安银行）会频繁出现假突破，金叉死叉交替，产生较多\"洗损\"交易，夏普偏低。\n",
    "2. **周期选择的权衡**：短周期越敏感，信号越多、反应越快，但噪音也多；长周期更平滑、过滤噪音，但滞后更大、容易错过行情起点。本实验中 (5,15) 与 (5,20) 反应灵敏，(10,60) 更稳健但信号稀少。值得注意的是，在剧烈波动的芯动联科上，过窄的止损（5%）会在震荡中反复触发，导致长周期（10/30）反而录得 −38.8%——说明止损比例需与标的波动特征匹配。\n",
    "3. **成本与止损的真实影响**：本 Notebook 已建模佣金、印花税、滑点与 5% 硬止损。成本会侵蚀收益，但硬止损能显著收敛最大回撤（无止损时芯动联科 5/15 回撤达 −40.5%，加入止损后收敛到 −28.7%），并避免了一次深跌使累计收益转正——这正是\"截断亏损、让利润奔跑\"的体现。\n",
    "4. **风险提示**：策略仍为后视回测，实盘需加入仓位管理与参数鲁棒性检验（如 Walk-forward）。双均线是理解\"趋势跟踪\"思想的起点，而非\"圣杯\"。\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
