A vol-normalised momentum strategy that each week ranks the 50 most-traded Bybit perpetuals by 30-day risk-adjusted return, goes long the top-5 winners and short the bottom-5 losers. Optimised via an 8-dimensional parameter sweep.
XSMOM applies a classic cross-sectional momentum framework to crypto perpetuals, with two deliberate asymmetries: a trend-confirmation filter on the long side, and no filter on the short side.
In crypto, relative momentum persists over multi-week horizons: coins that have outperformed on a risk-adjusted basis tend to keep outperforming, and the weakest tend to keep underperforming. Ranking by vol-normalised return (return ÷ volatility) rather than raw return avoids buying high-volatility noise — a coin that rose 30% on 60% daily vol is a weaker signal than one that rose 20% on 15% vol.
The vol-normalised metric can rank a coin highly because its volatility collapsed, not because it's trending up. The long side therefore requires the coin to be above its 50-day (now 100-day) moving average and have a positive raw 30-day return. The short side needs no filter: in a structurally risk-off altcoin regime, the weakest losers keep losing — shorting them has been the more reliable leg.
The full pipeline, from raw Bybit data to a ranked long/short book.
For each symbol, over the trailing 30 days ending at the rebalance date:
raw_return = (close[t] - close[t-30]) / close[t-30]
atr_pct = ATR(14) / close[t] # daily vol, as % of price
VNR = raw_return / (atr_pct × √30) # risk-adjusted momentum
VNR measures how far price moved per unit of volatility over the window — the standard "vol-normalised" momentum used in academic cross-sectional momentum studies.
close > MA(100) and raw_return > 0. Only coins with genuine upward momentum qualify.VNR ≤ -0.5 (optimised floor). In the current sample no coin met this threshold, so the optimised run is effectively long-only.Backtest of the optimised configuration over 21 biweekly periods (10 Nov 2025 → 17 Aug 2026). Data: real Bybit daily OHLCV for the top-50 volume perps; universe expands as newer perps accumulate enough history (33 → 47 symbols).
| Total P&L | +$2,513.65 |
| Long P&L | +$2,513.65 |
| Short P&L | $0.00 (flat — floor never triggered) |
| Periods (biweekly) | 21 |
| Avg P&L / period | +$119.70 |
| Win rate (trades) | 32.5% (27W / 56L) |
| Avg win | +$124.28 |
| Avg loss | -$15.03 |
| Profit factor | 3.99 |
| Max drawdown | -$284.42 |
| Sharpe (weekly, 0% Rf) | 0.236 |
| Sharpe (annualised ×√26) | ≈1.20 |
| Total trades | 83 (all long) |
| Month | Periods | P&L |
|---|---|---|
| 2025-11 | 2 | -$171.2 |
| 2025-12 | 2 | +$11.5 |
| 2026-01 | 2 | +$88.4 |
| 2026-02 | 2 | +$93.7 |
| 2026-03 | 3 | -$58.1 |
| 2026-04 | 2 | +$267.6 |
| 2026-05 | 2 | -$110.1 |
| 2026-06 | 2 | +$110.5 |
| 2026-07 | 2 | -$109.1 |
| 2026-08 | 2 | +$2,390.5 |
August's figure is almost entirely the single +$2,280 week.
| Rebalance date | Universe | Longs | Shorts | Period P&L | Cumulative |
|---|
50+ configurations × 24 weeks, one dimension at a time against the baseline (30d VNR · MA=50 · weekly · 5L/5S · no stop · no short floor · ma+rawpos). Rankings by Sharpe. In-sample optimisation — results are likely optimistic; treat as directional, not as expected live performance.
| Schedule | P&L | Sharpe | PF |
|---|---|---|---|
| weekly (baseline) | +$409 | 0.182 | 1.23 |
| biweekly | +$3,815 | 0.328 | 2.53 |
Extending the hold from 7→14 days lets winners compound and cuts rebalance whipsaw. Note: this leg of the sweep ran without the short floor, so shorts were still active here.
| Stop | P&L | Sharpe | PF | MaxDD |
|---|---|---|---|---|
| none (baseline) | +$410 | 0.182 | 1.23 | -$180 |
| 15% | +$830 | 0.371 | 1.60 | -$126 |
| 25% | +$597 | 0.262 | 1.37 | -$170 |
15% is the single best Sharpe improvement: it caps the catastrophic single-coin blow-ups (e.g. -247% SKRUSDT week in the unfiltered run) while letting winners run.
| MA | P&L | Sharpe | PF |
|---|---|---|---|
| none (raw-pos only) | +$364 | 0.156 | 1.20 |
| 20 | +$355 | 0.150 | 1.19 |
| 50 (baseline) | +$411 | 0.183 | 1.23 |
| 100 | +$525 | 0.221 | 1.32 |
| 200 | +$289 | 0.146 | 1.21 |
MA=100 is the sweet spot — a slower regime filter that rejects more false-positive longs in chop.
| Floor | P&L | Sharpe | PF | Trades |
|---|---|---|---|---|
| none (baseline) | +$410 | 0.182 | 1.23 | 239 |
| -0.3 | +$498 | 0.198 | 1.38 | 167 |
| -0.5 | +$555 | 0.199 | 1.49 | 119 |
| -0.75 | +$555 | 0.199 | 1.49 | 119 |
| -1.0 | +$555 | 0.199 | 1.49 | 119 |
Tighter floors cull weak/mean-reverting shorts and lift the profit factor from 1.23 → 1.49. Past -0.5 nothing changes: the weak cohorts simply stop qualifying.
| Window | P&L | Sharpe | MaxDD |
|---|---|---|---|
| 14 | +$472 | 0.144 | -$510 |
| 21 | +$48 | 0.019 | -$180 |
| 30 (baseline) | +$412 | 0.183 | -$178 |
| 45 | +$117 | 0.040 | -$329 |
| 60 | -$137 | -0.058 | -$272 |
30 days has the best Sharpe / drawdown balance. 14d has higher raw P&L but 2.9× the drawdown.
| Config | P&L | Sharpe |
|---|---|---|
| 3L/3S | +$287 | 0.194 |
| 5L/5S (baseline) | +$410 | 0.182 |
| 7L/7S | +$667 | 0.193 |
| cap 50% / 40% / 30% | +$393 / +$393 / +$387 | ≈0.177 / 0.175 |
Concentration caps hurt: signal-proportional sizing is an alpha source, not a risk bug. 7L/7S maximises raw P&L; 5L/5S is the risk/reward compromise.
| Config | P&L | Sharpe | WR | PF |
|---|---|---|---|---|
| Baseline (30d/MA50/weekly) | +$410 | 0.182 | 45.6% | 1.23 |
| Best-of-each-dimension (30d/MA100/biweekly/15%SL/floor-0.5) | +$2,488 | 0.285 | 41.2% | 2.43 |
| Applied optimised config (same, with floor active → long-only) | +$2,514 | 0.236 | 32.5% | 3.99 |
Full 50-row sweep data available at sweep_data.json in this repo.
Pure-Python, stdlib-only backtest (no pandas/numpy). Fetches real data from the Bybit V5 public REST API and caches it locally.
MOMSXperp/
├── config.py # strategy parameters (optimised)
├── run.py # backtest entry point
├── analysis.py # ranking, selection, sizing, P&L engine
├── fetch.py # Bybit V5 OHLCV fetch + local cache
├── report.py # extended diagnostics (monthly, streaks, conc.)
├── charts.py # matplotlib equity/side/drawdown charts
├── sweep.py # 8-dimension parameter sweep
├── xsmom_results.json # optimised backtest output
├── xsmom_sweep_results.json # full sweep output
└── README.md
# backtest (cached data; --force to re-fetch)
python3 -m xsmom.run
# extended diagnostics
python3 -m xsmom.report
# charts (needs matplotlib)
python3 -m xsmom.charts
# full parameter sweep (2-3 min)
python3 -m xsmom.sweep
Data is cached under xsmom/data_cache/ so repeat runs are instant. No API key required for public market data.
sweep.py reloads modules between runs so parameter changes take effect.Read this before acting on anything on this page. These are the results a critical reviewer should weigh.
Everything is open for review. Clone, re-run, and challenge the numbers.
xsmom_results.json — full optimised backtest, per-trade detail.xsmom_sweep_results.json — all 50 sweep configurations with weekly P&L series.sweep_data.json — condensed sweep tables used to build this page.config.py — the exact optimised parameter set.