"This instrument is too illiquid to backtest" is a conclusion you should be suspicious of.
I was building a 1-minute breakout study across commodities and FX. My first pass produced a median of 11 one-minute bars per trading day on gold futures.
A full futures session is about 1,380 one-minute bars. Eleven is 0.8% coverage. I wrote it down as "gold is too thin at 1-minute resolution, drop it" and moved on.
Gold is the most liquid commodity future in the world. It was not thin. My symbol was wrong.
What a continuous contract actually is
Historical futures data vendors give you a synthetic series stitched across contract months, so you can chart more than three months of history. Two common variants:
| suffix | what it does |
|---|---|
*.c.0 | continuation — just rolls from one front month to the next on the roll date |
*.v.0 | volume-weighted / ratio-adjusted — adjusts the historical prices so the series is continuous without gaps |
The continuation series is fine for a daily chart. At 1-minute resolution it has a specific pathology: on most days, the c.0 series is quoting a contract that is not the one people were trading.
I measured it as "bars per day" — the count of distinct 1-minute bars the series produced per session. A real, actively-traded contract produces ~1,380. A series that spends most of its time pointing at a nearly-dead back month produces almost nothing, because that contract barely trades.
The measurement
Same source, same period, same one-minute frequency. Only the symbol suffix changed.
| contract | *.c.0 bars/day | *.v.0 bars/day | multiplier |
|---|---|---|---|
| GC gold | 11 | 1,372 | 125× |
| MGC micro gold | 357 | 1,380 | 3.9× |
| SI silver | 10 | 1,292 | 129× |
| HG copper | 14 | 1,255 | 90× |
| 6E euro FX | 90 | 1,327 | 15× |
| 6B pound FX | 26 | 1,197 | 45× |
Note that NQ, ES, CL, NG, M6E and ZW were fine — their c.0 series produced ~1,380 bars/day all along, because those contracts roll in a way that keeps the continuation series pointed at liquid months. So this is not "c.0 is always broken". It is "c.0 is broken for some contracts and you have to check per contract".
That distinction matters, because the cheap version of this lesson ("always use v.0") is not what the data says. What the data says is: check the bar count per instrument before you believe anything about liquidity.
The diagnostic
One number tells you whether you are looking at a liquidity problem or a data problem:
median one-minute bars per trading day, as a fraction of a full session.
If it is above ~90%, the instrument is fine and any weirdness is downstream. If it is far below — 1%, 10%, 26% — do not conclude the market is thin. Conclude that your series is not pointing at the traded contract and go check the symbol.
Concretely:
bars_per_day = df.groupby(df.index.date).size() print(bars_per_day.median()) # ~1380 for a liquid futures contract Run that before you run any strategy. It takes seconds and it separates "this market has no liquidity" from "I downloaded the wrong thing". Those two conclusions have completely different follow-ups, and the second one is much more common.
What I got wrong twice
First error: attributing to the market what was caused by the symbol. I had a plausible story — some commodities really are thin — and the story made the measurement easy to accept. The 11 bars looked like evidence for the story.
Second error: applying the fix too broadly. After finding this, I briefly believed no c.0 series was usable. That is false: six of my contracts were fine. Over-correcting would have meant re-downloading data I already had, which is not free.
The corrective habit is narrow and cheap: check the bar count per instrument. Not per asset class, not per vendor. Per instrument, once, before you trust it.
A related trap in the same family
While fixing this I found the neighbouring problem, which is worse because it does not show up as missing data:
The c.0 series' price level can differ from the actual traded contract's price by a non-trivial amount — I measured about 0.6% between the continuous gold series and the active month. That does not reduce your bar count. It silently shifts your levels, so support and resistance levels computed on the synthetic series do not correspond to prices anyone could have traded.
Missing data announces itself. Displaced prices do not.
So the check list for any vendor-supplied continuous series is two items, not one:
- Bars per day — is the series actually populated?
- Price level versus the front month — is the series quoting prices that existed?
Both are cheap. Neither is in the vendor's documentation.
Bar-count-per-day is the first thing I check on any new dataset, and it is why the demo data in the open-source linter ships with a gap check (GitHub, MIT, no dependencies): a series that is 1% populated will produce a backtest full of technical artefacts that look like opportunities.
If you would rather have this as a download than a repo: the same tool, plus the
demo trade logs (one with five bugs planted, one clean), is *$0** on Gumroad —
pay what you want, and $0 is a valid answer: https://weiwave23.gumroad.com/l/backtest-lie-detector*


