DEV Community

王伟
王伟

Posted on

Gold Is Not Illiquid — You Picked the Wrong Symbol

"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.

Bars per day, before and after the symbol change

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Bars per day — is the series actually populated?
  2. 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*

Top comments (0)