{ "cells": [ { "cell_type": "markdown", "id": "0327ce80", "metadata": {}, "source": [ "---\n", "title: Multi-Factor Long/Short Equity\n", "summary: The classic dollar-neutral combination of value, quality, and momentum — the flagship long-short format.\n", "tags: [multifactor, long-short, value, quality, momentum]\n", "---\n", "\n", "# Multi-Factor Long/Short Equity\n", "\n", "The bread-and-butter of systematic equity: don't bet on one signal, **combine several**. We z-score\n", "value, quality, and momentum, add them into a single composite score, and build a **dollar-neutral**\n", "long/short book — long the best names, short the worst. Diversifying across factors smooths the ride\n", "when any one of them is out of favor." ] }, { "cell_type": "code", "execution_count": null, "id": "73297dcd", "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": "6cace286", "metadata": {}, "source": [ "## The idea\n", "\n", "Each factor is already cross-sectionally standardized each day, so we can simply **sum** them into a\n", "composite and sort on it. Value (`val_bm`) buys cheap, quality (`qual_roe`) buys profitable, momentum\n", "(`mom_12m`) buys winners — three largely independent edges in one book." ] }, { "cell_type": "code", "execution_count": null, "id": "bee9cc0b", "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", " \"\"\"Composite of value + quality + momentum, dollar-neutral top/bottom quintile.\"\"\"\n", " weights = {\"val_bm\": 1.0, \"qual_roe\": 1.0, \"mom_12m\": 1.0}\n", "\n", " def on_day(self, day, features, prices, portfolio):\n", " n = len(prices)\n", " combo = np.zeros(n)\n", " for name, wt in self.weights.items():\n", " combo += wt * np.nan_to_num(features.get(name, np.zeros(n)))\n", " return _long_short(combo, frac=0.2)" ] }, { "cell_type": "markdown", "id": "d8aca91e", "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": "1b2225ef", "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", "# How much does each factor contribute? Compare the composite to each factor alone.\n", "single = {}\n", "for f in [\"val_bm\", \"qual_roe\", \"mom_12m\"]:\n", " class _S(MyStrategy):\n", " weights = {f: 1.0}\n", " single[f] = Grader(market).evaluate(_S()).oos_sharpe\n", "print(\"OOS Sharpe — single factors:\", {k: round(v, 2) for k, v in single.items()})\n", "print(\"OOS Sharpe — composite :\", round(report.oos_sharpe, 2))\n", "print(\"Combining factors smooths in-sample — but beating OOS is hard; watch the overfitting ratio.\")" ] }, { "cell_type": "markdown", "id": "bbf4e3c8", "metadata": {}, "source": [ "## What I'd try next\n", "\n", "- **Weight the factors** by recent IC instead of equally.\n", "- **Neutralize** the book to size or sector before sorting.\n", "- Add a **volatility scale** so the book targets constant risk.\n", "- Watch the overfitting ratio as you add factors — more knobs = more ways to fool yourself." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }