{ "cells": [ { "cell_type": "markdown", "id": "67813042", "metadata": {}, "source": [ "---\n", "title: Short-Term Reversal\n", "summary: Buy last week's losers, short last week's winners — the classic weekly mean-reversion effect.\n", "tags: [reversal, mean-reversion, short-term]\n", "---\n", "\n", "# Short-Term Reversal\n", "\n", "Over short horizons, stocks that just fell tend to bounce and stocks that just jumped tend to give\n", "some back — liquidity provision and overreaction. We trade the **weekly reversal** signal: long recent\n", "losers, short recent winners. It's a high-turnover effect, so it lives or dies on costs." ] }, { "cell_type": "code", "execution_count": null, "id": "dde8ab74", "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": "89261e76", "metadata": {}, "source": [ "## The idea\n", "\n", "The `reversal_1w` feature is already sign-flipped so that **high values = recent losers**. So we just\n", "go long the top of that signal and short the bottom — buying the losers, shorting the winners." ] }, { "cell_type": "code", "execution_count": null, "id": "eb1f6051", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from convexpi.lab import Strategy\n", "\n", "def _long_short(signal, frac=0.2):\n", " \"\"\"Dollar-neutral long/short: long the top `frac`, short the bottom `frac`, equal-weighted.\"\"\"\n", " s = np.nan_to_num(np.asarray(signal, dtype=float))\n", " n = len(s); k = max(1, int(n * frac))\n", " order = np.argsort(s)\n", " w = np.zeros(n); w[order[-k:]] = 1.0 / k; w[order[:k]] = -1.0 / k\n", " return w\n", "\n", "class MyStrategy(Strategy):\n", " \"\"\"Weekly reversal: long recent losers (high reversal_1w), short recent winners.\"\"\"\n", " def on_day(self, day, features, prices, portfolio):\n", " sig = features.get(\"reversal_1w\", np.zeros(len(prices)))\n", " return _long_short(sig, frac=0.2)" ] }, { "cell_type": "markdown", "id": "bd1e86e4", "metadata": {}, "source": [ "## Out-of-sample evaluation\n", "\n", "Train on the first half of a synthetic market, evaluate on the held-out second half — the same discipline as the leaderboard." ] }, { "cell_type": "code", "execution_count": null, "id": "a2a649f9", "metadata": {}, "outputs": [], "source": [ "from convexpi.lab import SyntheticMarket, Grader\n", "market = SyntheticMarket(n_stocks=80, n_days=1800, seed=1)\n", "report = Grader(market).evaluate(MyStrategy())\n", "print(f\"in-sample Sharpe : {report.is_sharpe:+.2f}\")\n", "print(f\"out-of-sample Sharpe: {report.oos_sharpe:+.2f}\")\n", "print(f\"overfitting ratio : {report.overfitting_ratio:+.2f}\")\n", "oos = report.oos_result.daily_returns\n", "fig, ax = plt.subplots(figsize=(8, 3))\n", "ax.plot(np.cumprod(1 + oos)); ax.set_title(\"Out-of-sample equity curve\"); ax.set_ylabel(\"growth of $1\")\n", "plt.tight_layout(); plt.show()\n", "\n", "print(\"Reversal is high-turnover: realistic transaction costs eat much of the gross edge.\")\n", "print(\"Try widening the quintile (frac) or trading less often to cut turnover.\")" ] }, { "cell_type": "markdown", "id": "f77668c8", "metadata": {}, "source": [ "## What I'd try next\n", "\n", "- **Costs dominate** here — estimate turnover and stress-test net-of-cost performance.\n", "- Trade only the **most liquid** names where the bounce is real, not a bid-ask artifact.\n", "- Blend with a slower signal (momentum) so the two horizons offset each other." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }