ConvexPi

Short-Term Reversal

— Baseline —strategyreversalmean-reversionshort-term

Short-Term Reversal

Over short horizons, stocks that just fell tend to bounce and stocks that just jumped tend to give some back — liquidity provision and overreaction. We trade the weekly reversal signal: long recent losers, short recent winners. It's a high-turnover effect, so it lives or dies on costs.

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

The reversal_1w feature is already sign-flipped so that high values = recent losers. So we just go long the top of that signal and short the bottom — buying the losers, shorting the winners.

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):
    """Weekly reversal: long recent losers (high reversal_1w), short recent winners."""
    def on_day(self, day, features, prices, portfolio):
        sig = features.get("reversal_1w", np.zeros(len(prices)))
        return _long_short(sig, 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()

print("Reversal is high-turnover: realistic transaction costs eat much of the gross edge.")
print("Try widening the quintile (frac) or trading less often to cut turnover.")
in-sample Sharpe    : -2.47
out-of-sample Sharpe: +0.76
overfitting ratio   : +0.00
No description has been provided for this image
Reversal is high-turnover: realistic transaction costs eat much of the gross edge.
Try widening the quintile (frac) or trading less often to cut turnover.

What I'd try next

  • Costs dominate here — estimate turnover and stress-test net-of-cost performance.
  • Trade only the most liquid names where the bounce is real, not a bid-ask artifact.
  • Blend with a slower signal (momentum) so the two horizons offset each other.

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.