ConvexPi

Multi-Factor Long/Short Equity

— Baseline —strategymultifactorlong-shortvaluequalitymomentum

Multi-Factor Long/Short Equity

The bread-and-butter of systematic equity: don't bet on one signal, combine several. We z-score value, quality, and momentum, add them into a single composite score, and build a dollar-neutral long/short book — long the best names, short the worst. Diversifying across factors smooths the ride when any one of them is out of favor.

In [1]:
try:
    import convexpi.lab  # noqa
except ImportError:
    import subprocess, sys
    subprocess.run([sys.executable, "-m", "pip", "install", "-q", "convexpi-lab"])
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
print("ready")
ready

The idea

Each factor is already cross-sectionally standardized each day, so we can simply sum them into a composite and sort on it. Value (val_bm) buys cheap, quality (qual_roe) buys profitable, momentum (mom_12m) buys winners — three largely independent edges in one book.

In [2]:
import numpy as np
from convexpi.lab import Strategy

def _long_short(signal, frac=0.2):
    """Dollar-neutral long/short: long the top `frac`, short the bottom `frac`, equal-weighted."""
    s = np.nan_to_num(np.asarray(signal, dtype=float))
    n = len(s); k = max(1, int(n * frac))
    order = np.argsort(s)
    w = np.zeros(n); w[order[-k:]] = 1.0 / k; w[order[:k]] = -1.0 / k
    return w

class MyStrategy(Strategy):
    """Composite of value + quality + momentum, dollar-neutral top/bottom quintile."""
    weights = {"val_bm": 1.0, "qual_roe": 1.0, "mom_12m": 1.0}

    def on_day(self, day, features, prices, portfolio):
        n = len(prices)
        combo = np.zeros(n)
        for name, wt in self.weights.items():
            combo += wt * np.nan_to_num(features.get(name, np.zeros(n)))
        return _long_short(combo, frac=0.2)

Out-of-sample evaluation

Train on the first half of a synthetic market, evaluate on the held-out second half — the same discipline as the leaderboard.

In [3]:
from convexpi.lab import SyntheticMarket, Grader
market = SyntheticMarket(n_stocks=80, n_days=1800, seed=1)
report = Grader(market).evaluate(MyStrategy())
print(f"in-sample Sharpe    : {report.is_sharpe:+.2f}")
print(f"out-of-sample Sharpe: {report.oos_sharpe:+.2f}")
print(f"overfitting ratio   : {report.overfitting_ratio:+.2f}")
oos = report.oos_result.daily_returns
fig, ax = plt.subplots(figsize=(8, 3))
ax.plot(np.cumprod(1 + oos)); ax.set_title("Out-of-sample equity curve"); ax.set_ylabel("growth of $1")
plt.tight_layout(); plt.show()

# How much does each factor contribute? Compare the composite to each factor alone.
single = {}
for f in ["val_bm", "qual_roe", "mom_12m"]:
    class _S(MyStrategy):
        weights = {f: 1.0}
    single[f] = Grader(market).evaluate(_S()).oos_sharpe
print("OOS Sharpe — single factors:", {k: round(v, 2) for k, v in single.items()})
print("OOS Sharpe — composite     :", round(report.oos_sharpe, 2))
print("Combining factors smooths in-sample — but beating OOS is hard; watch the overfitting ratio.")
in-sample Sharpe    : +0.11
out-of-sample Sharpe: -0.47
overfitting ratio   : -4.28
No description has been provided for this image
OOS Sharpe — single factors: {'val_bm': -0.49, 'qual_roe': -0.05, 'mom_12m': -1.39}
OOS Sharpe — composite     : -0.47
Combining factors smooths in-sample — but beating OOS is hard; watch the overfitting ratio.

What I'd try next

  • Weight the factors by recent IC instead of equally.
  • Neutralize the book to size or sector before sorting.
  • Add a volatility scale so the book targets constant risk.
  • Watch the overfitting ratio as you add factors — more knobs = more ways to fool yourself.

Make it yours: open in Colab, then File → Save a copy in Drive (or in GitHub) to get your own editable copy.

Discussion (0)

Keep feedback constructive: what worked, what you'd try next, or a specific question.

Sign in to join the discussion.

No comments yet — be the first.