{ "cells": [ { "cell_type": "markdown", "id": "1e495ff0", "metadata": {}, "source": [ "---\n", "title: Momentum + Value — a Strategy That Survives Out of Sample\n", "summary: Short-horizon momentum combined with value, in a wide dollar-neutral book — a real edge that holds up out of sample (most of the time).\n", "tags: [momentum, value, multifactor, out-of-sample]\n", "---\n", "\n", "# Momentum + Value — a Strategy That Survives Out of Sample\n", "\n", "Most of the classic single factors look great in a backtest and then evaporate out of sample (see the\n", "other examples). This one is different: **short-horizon momentum** (last month's winners keep winning,\n", "briefly) blended with **value** (cheap beats expensive) is one of the most durable combinations in\n", "equities. We hold a *wide* dollar-neutral book and let weekly rebalancing keep turnover in check — and\n", "we check that the edge **survives out of sample across many markets**, not just one lucky draw." ] }, { "cell_type": "code", "execution_count": null, "id": "ee44e66e", "metadata": {}, "outputs": [], "source": [ "try:\n", " import convexpi.lab # noqa\n", "except ImportError:\n", " import subprocess, sys\n", " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"convexpi-lab\"])\n", "%matplotlib inline\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "print(\"ready\")" ] }, { "cell_type": "markdown", "id": "2606fef3", "metadata": {}, "source": [ "## The strategy\n", "\n", "Combine two real edges into one score and trade the spread. We weight short-horizon momentum\n", "(`mom_1m`) a bit more than value (`val_bm`), and go long the top half / short the bottom half — a wide\n", "book diversifies away single-name noise, and weekly rebalancing (the grader's default) keeps the\n", "high-turnover momentum leg from being eaten by costs." ] }, { "cell_type": "code", "execution_count": null, "id": "668193d0", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from convexpi.lab import Strategy\n", "\n", "class MyStrategy(Strategy):\n", " \"\"\"Short-horizon momentum + value, dollar-neutral, wide book (top/bottom half).\"\"\"\n", " weights = {\"mom_1m\": 1.0, \"val_bm\": 0.5}\n", " frac = 0.5\n", "\n", " def on_day(self, day, features, prices, portfolio):\n", " n = len(prices)\n", " score = np.zeros(n)\n", " for name, wt in self.weights.items():\n", " score += wt * np.nan_to_num(features.get(name, np.zeros(n)))\n", " k = max(1, int(n * self.frac))\n", " order = np.argsort(score)\n", " w = np.zeros(n)\n", " w[order[-k:]] = 1.0 / k # long the strongest\n", " w[order[:k]] = -1.0 / k # short the weakest\n", " return w" ] }, { "cell_type": "markdown", "id": "964f44f9", "metadata": {}, "source": [ "## Does it survive? Check across many markets\n", "\n", "A single backtest can be lucky. The honest test is **robustness**: evaluate out of sample on many\n", "independent synthetic markets and look at the *distribution* of OOS Sharpe — not one number." ] }, { "cell_type": "code", "execution_count": null, "id": "a1d95c3c", "metadata": {}, "outputs": [], "source": [ "from convexpi.lab import SyntheticMarket, Grader\n", "\n", "oos = []\n", "for seed in range(1, 21):\n", " r = Grader(SyntheticMarket(n_stocks=200, n_days=1260, seed=seed)).evaluate(MyStrategy())\n", " oos.append(r.oos_sharpe)\n", "oos = np.array(oos)\n", "\n", "print(f\"out-of-sample Sharpe across 20 markets:\")\n", "print(f\" mean : {oos.mean():+.2f}\")\n", "print(f\" win rate : {np.mean(oos > 0):.0%} (fraction of markets with positive OOS)\")\n", "print(f\" range : [{oos.min():+.2f}, {oos.max():+.2f}]\")\n", "\n", "fig, ax = plt.subplots(figsize=(8, 3))\n", "ax.hist(oos, bins=10, color=\"steelblue\"); ax.axvline(0, color=\"grey\", lw=1)\n", "ax.axvline(oos.mean(), color=\"darkorange\", lw=2, label=f\"mean {oos.mean():+.2f}\")\n", "ax.set_title(\"OOS Sharpe distribution (20 markets)\"); ax.set_xlabel(\"OOS Sharpe\"); ax.legend()\n", "plt.tight_layout(); plt.show()" ] }, { "cell_type": "markdown", "id": "20eb3628", "metadata": {}, "source": [ "## The honest part\n", "\n", "Positive **on average and most of the time** — but look at the left tail: some markets still hand it a\n", "losing draw. That's the truth about even good strategies: an edge is a *tilt in the distribution*, not\n", "a guarantee. The permanent leaderboard grades this on one hidden market, so the badge you see is one\n", "draw from that histogram.\n", "\n", "## What I'd try next\n", "\n", "- Add a third lightly-weighted factor and see if it tightens the distribution (lifts the left tail).\n", "- Vary the book width (`frac`) and rebalance horizon to trade off noise vs. turnover cost.\n", "- Compare against the single-factor examples — diversification is what turns a coin-flip into an edge." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }