{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 📘 Fincept Notebook — Moving Average Crossover Backtest\n",
    "\n",
    "**Trading · Intermediate · ~22 min · pandas + numpy**\n",
    "\n",
    "The moving-average crossover is the classic trend-following strategy: go long when a fast average crosses above a slow one, and step aside when it crosses back below. In this notebook we generate a reproducible price series, build the signals with pandas, and *backtest* the strategy against simply buying and holding.\n",
    "\n",
    "**What you'll learn**\n",
    "- Compute fast/slow simple moving averages with pandas `rolling`\n",
    "- Turn a crossover into long/flat signals and lag them to avoid look-ahead bias\n",
    "- Build equity curves and compare strategy vs buy-and-hold\n",
    "- Report total & annualized return, max drawdown, and a simple Sharpe ratio\n",
    "\n",
    "> **Requires** `pandas` and `numpy` (bundled with Fincept Terminal's Python environment).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. A reproducible price series\n",
    "\n",
    "We synthesise 120 trading days of prices with a gentle upward **drift** plus daily **noise**, using `np.random.seed(7)` so the series is identical every run — and identical in every cell, since each cell regenerates it from the same seed. The result is a single random walk that trends up but with realistic pullbacks for the crossover to react to.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {"fincept_title": "Generate prices"},
   "outputs": [],
   "source": [
    "try:\n",
    "    import numpy as np\n",
    "    import pandas as pd\n",
    "except ImportError:\n",
    "    raise SystemExit(\"Fincept Notebook needs pandas + numpy — install with: pip install pandas numpy\")\n",
    "\n",
    "def make_prices(n=120, start=100.0, drift=0.0006, vol=0.012, seed=7):\n",
    "    np.random.seed(seed)\n",
    "    shocks = np.random.normal(drift, vol, n)\n",
    "    prices = start * np.cumprod(1 + shocks)\n",
    "    dates = pd.bdate_range(\"2024-01-01\", periods=n)\n",
    "    return pd.Series(prices, index=dates, name=\"close\")\n",
    "\n",
    "px = make_prices()\n",
    "print(\"Synthetic daily closes (120 business days)\")\n",
    "print(\"=\" * 44)\n",
    "print(f\"start  {px.iloc[0]:8.2f}\")\n",
    "print(f\"end    {px.iloc[-1]:8.2f}\")\n",
    "print(f\"min    {px.min():8.2f}\")\n",
    "print(f\"max    {px.max():8.2f}\")\n",
    "print()\n",
    "print(\"First 5 closes:\")\n",
    "print(px.head().to_string())\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Fast & slow moving averages and the signal\n",
    "\n",
    "We compute a **fast SMA (10-day)** and a **slow SMA (30-day)** with `Series.rolling(window).mean()`. The position is **long (1)** whenever the fast SMA is above the slow SMA, otherwise **flat (0)**. Critically we `shift(1)` the signal: you can only act on a crossover the *next* day, so trading on today's bar with today's signal would be cheating (look-ahead bias).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {"fincept_title": "SMAs & signal"},
   "outputs": [],
   "source": [
    "try:\n",
    "    import numpy as np\n",
    "    import pandas as pd\n",
    "except ImportError:\n",
    "    raise SystemExit(\"Fincept Notebook needs pandas + numpy — install with: pip install pandas numpy\")\n",
    "\n",
    "def make_prices(n=120, start=100.0, drift=0.0006, vol=0.012, seed=7):\n",
    "    np.random.seed(seed)\n",
    "    shocks = np.random.normal(drift, vol, n)\n",
    "    prices = start * np.cumprod(1 + shocks)\n",
    "    return pd.Series(prices, index=pd.bdate_range(\"2024-01-01\", periods=n), name=\"close\")\n",
    "\n",
    "FAST, SLOW = 10, 30\n",
    "df = make_prices().to_frame()\n",
    "df[\"sma_fast\"] = df[\"close\"].rolling(FAST).mean()\n",
    "df[\"sma_slow\"] = df[\"close\"].rolling(SLOW).mean()\n",
    "df[\"signal\"] = (df[\"sma_fast\"] > df[\"sma_slow\"]).astype(int)\n",
    "df[\"position\"] = df[\"signal\"].shift(1).fillna(0)  # act next day\n",
    "\n",
    "trades = int((df[\"position\"].diff().abs() > 0).sum())\n",
    "print(f\"Crossover signals (FAST={FAST}, SLOW={SLOW}) — tail of the table\")\n",
    "print(\"=\" * 60)\n",
    "print(df.tail(8).round(2).to_string())\n",
    "print()\n",
    "print(f\"Days long: {int(df['position'].sum())} of {len(df)}   |   position changes: {trades}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Equity curves: strategy vs buy-and-hold\n",
    "\n",
    "The market's daily return is `close.pct_change()`. The strategy only earns that return on days it was positioned long, so `strategy_return = position * market_return`. Compounding each stream gives an **equity curve** (growth of \\$1). We compare the crossover strategy with simply buying and holding from day one.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {"fincept_title": "Equity curves"},
   "outputs": [],
   "source": [
    "try:\n",
    "    import numpy as np\n",
    "    import pandas as pd\n",
    "except ImportError:\n",
    "    raise SystemExit(\"Fincept Notebook needs pandas + numpy — install with: pip install pandas numpy\")\n",
    "\n",
    "def make_prices(n=120, start=100.0, drift=0.0006, vol=0.012, seed=7):\n",
    "    np.random.seed(seed)\n",
    "    shocks = np.random.normal(drift, vol, n)\n",
    "    prices = start * np.cumprod(1 + shocks)\n",
    "    return pd.Series(prices, index=pd.bdate_range(\"2024-01-01\", periods=n), name=\"close\")\n",
    "\n",
    "FAST, SLOW = 10, 30\n",
    "df = make_prices().to_frame()\n",
    "df[\"sma_fast\"] = df[\"close\"].rolling(FAST).mean()\n",
    "df[\"sma_slow\"] = df[\"close\"].rolling(SLOW).mean()\n",
    "df[\"position\"] = (df[\"sma_fast\"] > df[\"sma_slow\"]).astype(int).shift(1).fillna(0)\n",
    "\n",
    "df[\"mkt_ret\"] = df[\"close\"].pct_change().fillna(0)\n",
    "df[\"strat_ret\"] = df[\"position\"] * df[\"mkt_ret\"]\n",
    "df[\"equity_hold\"] = (1 + df[\"mkt_ret\"]).cumprod()\n",
    "df[\"equity_strat\"] = (1 + df[\"strat_ret\"]).cumprod()\n",
    "\n",
    "print(\"Growth of $1 — last 8 days\")\n",
    "print(\"=\" * 44)\n",
    "print(df[[\"close\", \"position\", \"equity_hold\", \"equity_strat\"]].tail(8).round(3).to_string())\n",
    "print()\n",
    "print(f\"Final $1 -> buy & hold : ${df['equity_hold'].iloc[-1]:.3f}\")\n",
    "print(f\"Final $1 -> crossover  : ${df['equity_strat'].iloc[-1]:.3f}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Performance metrics\n",
    "\n",
    "Numbers, not vibes. We report for each approach:\n",
    "\n",
    "- **Total return** — final equity minus 1.\n",
    "- **Annualized return** — geometric, scaled to 252 trading days.\n",
    "- **Max drawdown** — the worst peak-to-trough loss along the equity curve.\n",
    "- **Sharpe ratio** — mean daily return / daily volatility, annualized by √252 (risk-free assumed 0).\n",
    "\n",
    "Trend-following often trails buy-and-hold in a smoothly rising market because it sits in cash during pullbacks — but it usually suffers a *smaller drawdown*, which is the trade-off this table makes visible.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {"fincept_title": "Metrics table"},
   "outputs": [],
   "source": [
    "try:\n",
    "    import numpy as np\n",
    "    import pandas as pd\n",
    "except ImportError:\n",
    "    raise SystemExit(\"Fincept Notebook needs pandas + numpy — install with: pip install pandas numpy\")\n",
    "\n",
    "def make_prices(n=120, start=100.0, drift=0.0006, vol=0.012, seed=7):\n",
    "    np.random.seed(seed)\n",
    "    shocks = np.random.normal(drift, vol, n)\n",
    "    prices = start * np.cumprod(1 + shocks)\n",
    "    return pd.Series(prices, index=pd.bdate_range(\"2024-01-01\", periods=n), name=\"close\")\n",
    "\n",
    "FAST, SLOW = 10, 30\n",
    "df = make_prices().to_frame()\n",
    "df[\"sma_fast\"] = df[\"close\"].rolling(FAST).mean()\n",
    "df[\"sma_slow\"] = df[\"close\"].rolling(SLOW).mean()\n",
    "df[\"position\"] = (df[\"sma_fast\"] > df[\"sma_slow\"]).astype(int).shift(1).fillna(0)\n",
    "df[\"mkt_ret\"] = df[\"close\"].pct_change().fillna(0)\n",
    "df[\"strat_ret\"] = df[\"position\"] * df[\"mkt_ret\"]\n",
    "\n",
    "def max_drawdown(returns):\n",
    "    equity = (1 + returns).cumprod()\n",
    "    peak = equity.cummax()\n",
    "    return ((equity - peak) / peak).min()\n",
    "\n",
    "def metrics(returns):\n",
    "    n = len(returns)\n",
    "    total = (1 + returns).prod() - 1\n",
    "    ann = (1 + total) ** (252 / n) - 1\n",
    "    vol = returns.std(ddof=0)\n",
    "    sharpe = (returns.mean() / vol * np.sqrt(252)) if vol > 0 else float(\"nan\")\n",
    "    return total, ann, max_drawdown(returns), sharpe\n",
    "\n",
    "rows = []\n",
    "for name, col in [(\"Buy & hold\", \"mkt_ret\"), (\"SMA crossover\", \"strat_ret\")]:\n",
    "    total, ann, mdd, sharpe = metrics(df[col])\n",
    "    rows.append({\n",
    "        \"Strategy\": name,\n",
    "        \"Total %\": round(total * 100, 2),\n",
    "        \"Annualized %\": round(ann * 100, 2),\n",
    "        \"Max DD %\": round(mdd * 100, 2),\n",
    "        \"Sharpe\": round(sharpe, 2),\n",
    "    })\n",
    "\n",
    "table = pd.DataFrame(rows).set_index(\"Strategy\")\n",
    "print(\"Backtest performance metrics\")\n",
    "print(\"=\" * 60)\n",
    "print(table.to_string())\n",
    "print()\n",
    "print(\"Note: a single random path is not evidence — robust backtesting runs\")\n",
    "print(\"many paths and includes costs, slippage, and out-of-sample testing.\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "*— Fincept Notebook · part of Fincept Terminal. Edit any cell and press Ctrl+Enter to run.*\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
  "language_info": {"name": "python"}
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
