You sent me the notebook Sunday night, and I've had it open on the second monitor since. Cross-sectional momentum, top 150 Binance USDⓈ-M perps by 30-day dollar volume, weekly rebalance, long the top decile short the bottom, 2021-01 through 2026-06. Sharpe 2.31, max drawdown 14.2%, and a cost model that looked honest to me: taker in, taker out, funding accrued per interval, a slippage term that scales with your size against the book. You did the hard parts right. Then you asked why six weeks of paper trading has come out flat, and whether the edge decayed.
It didn't decay. It was never in the backtest. Look at cell 4:
info = requests.get(BASE + "/fapi/v1/exchangeInfo").json()
symbols = [s["symbol"] for s in info["symbols"]
if s["status"] == "TRADING" and s["quoteAsset"] == "USDT"]
You called that endpoint in June 2026 and used the answer to define what was tradable in March 2021. Every symbol on that list survived to June 2026, by construction. That's the whole bug, and it's worth more than the rest of your cost model combined.
What the endpoint won't tell you
There is no history in exchangeInfo. No asOf parameter, no archive, no changelog. It is a photograph of right now, and Binance has never promised you anything else. When a contract is delisted, its entry is deleted from the response and the ticker stops existing as far as the API is concerned. Binance has listed north of 600 USDⓈ-M perpetuals since 2019 and retired well over a hundred of them. BTS, COCOS, TOMO, RAY, FTT, SC, a long tail of 2021 alt-season contracts that had one glorious quarter and then thinned out until the venue pulled them.
Ask yourself which names a 30-day momentum screen loves. Not BTC. It loves the thing that just tripled on a listing pump and a Twitter cycle. That population and the population of eventual delistings overlap heavily, and your universe filter removed the overlap.
I rebuilt your run against our snapshot archive, point-in-time universe, same signal, same costs, and the Sharpe came out 0.74 with a 31% drawdown. Roughly two fifths of the gap is delisted names you were never allowed to hold. Another quarter is the mirror-image problem, which is subtler and which I suspect you'll like less.
The other end of the time machine: symbols that didn't exist yet
Your feature is a 90-day return. Your rolling window has min_periods=20, because you set it once to stop the warmup from nuking the first quarter of the sample and never revisited it. So a contract that onboarded 21 days ago gets a momentum score computed off three weeks of post-listing price action, ranks in the top decile roughly whenever the listing went well, and enters your book.
That part is arguably legitimate. Here's what isn't: your provider backfilled some of those symbols with spot or index data from before the perp existed, so a handful of contracts have kline history predating their own onboardDate. You can check this in about a minute. Join your price table against onboard dates and count rows to the left of the listing. In your data there are 41 symbols with pre-listing bars, and one of them, in the summer of 2023, contributes a 6% single-week gain to the equity curve from a position in a contract that would not exist for another nine days.
A ticker string is not an identifier. It's a label the exchange rents out, sometimes twice.
Which brings up renames. MATICUSDT became POLUSDT. FTMUSDT became SUSDT on a 1:1 conversion. The LUNA situation in May 2022 left a LUNCUSDT and later a brand-new LUNAUSDT that shares a ticker root with something that lost more or less everything. If your loader keys on the symbol string and concatenates whatever files it finds, you have at least one series in there with a discontinuity that isn't a price move, and your momentum feature will read that discontinuity as the strongest signal in the cross-section.
Everything else that drifts under you
Once you accept that the symbol list is time-varying, the same argument applies to every other field in that response. You're using today's values for all of them.
| Field | How it drifts | What breaks |
|---|---|---|
| status | TRADING → SETTLING → gone | Survivorship; phantom exits at a close price that never traded |
| onboardDate | New listings weekly; absent for dead symbols | Trading contracts before they existed |
| tickSize / stepSize | Retuned as price levels change | Order rounding, and limit prices that would have been rejected |
| minNotional | Raised over time on thin books | Small legs your live router would refuse |
| fundingIntervalHours | 8h for years, then 4h or 1h on many symbols | Carry off by 2–3× on exactly the alts your screen holds |
| leverage brackets | Tiers and maintenance margin revised | Liquidation modelling and margin capacity |
The funding one hurts most in your case. Your accrual loop assumes three payments a day for the entire history. A meaningful slice of the alt book moved to four-hour funding, and short legs in high-funding names are where a lot of your simulated PnL comes from. You're not wrong by a rounding error there. You're wrong by a factor.
The delisting itself is an event, and you're not modelling it
In your point-in-time rerun I gave the strategy a generous exit. Real delistings run on a script: an announcement, usually seven to fourteen days out, then a reduce-only window, then forced settlement at a mark price. The announcement is public information and you can act on it, so the honest simulation is an exit at the close of the announcement day. But that close is already 10-20% below the prior week on a typical alt delisting, the book is thin, and your slippage term needs to know it's operating in a different regime. If your fill engine hands you the settlement price with no impact, you've quietly made dying assets tradable at fair value.
If you never archived exchangeInfo, you're not sunk. The public dump at data.binance.vision/data/futures/um/monthly/klines/ still keeps directories for delisted symbols long after the API has forgotten them. Enumerate the directory listing, and the first and last monthly file per symbol gives you a workable listing and delisting window without any vendor. It's a reconstruction, not a record, and it won't recover tick sizes or funding intervals. But it will tell you who existed when, which is 80% of what you need on Monday.
What I'd have you build before you touch the signal again
- A cron that pulls exchangeInfo from every venue you research, daily, and writes it to object storage keyed by date. Gzipped it's under half a megabyte. Ten years costs you a rounding error in S3 and buys you a research capability you cannot purchase later.
- An asset master derived from those snapshots: one row per (venue, symbol, valid_from, valid_to) with the full field set. Diff consecutive snapshots to generate it, and treat any field change as a new row.
- A universe function with a mandatory timestamp argument.
universe(ts), neveruniverse(). Make the default impossible so nobody, including future you at 1am, can reach for the survivor list by accident. - A pre-listing assertion in the data loader: no bar may exist before
onboard_tsminus one day. Fail the run, don't warn. - A stable internal instrument ID that survives renames, with the ticker demoted to an attribute. Map POL and MATIC to one ID, and flag redenominations so continuity logic can refuse to join across them.
Do those and rerun. My guess is you land near the 0.74 I got, and the interesting question becomes whether a 0.74 that includes the dead names has anything in it worth paper-trading. It might. Cross-sectional momentum in perps isn't nothing, and a chunk of what's left after you fix the universe is genuine carry from the short side. You'll also find your paper results and your backtest start agreeing, because paper trading has always run on a point-in-time universe. It never had a choice.
P.S. This isn't a crypto quirk, it's just louder here. Equity researchers have been fighting delisting returns and recycled tickers since CRSP started shipping them, and prediction markets take it to the limit: every contract expires by design, so the universe is nothing but listings and deaths. If you ever port this screen to Kalshi, build the asset master first. There's no other kind of history there.
← All posts


