Featured image of post My Quant Platform Was Throwing Away 92% of the Trades — and Believed It Saw Everything

My Quant Platform Was Throwing Away 92% of the Trades — and Believed It Saw Everything

A silent filter meant AlphaTrace only ever saw 8% of an on-chain address's behavior. This is the post-mortem of a self-correction: from a signal-arbitrage article that inspired the work, to a live-data check that revealed ingest-time data loss, to the inference I almost published as fact.

New post

TL;DR

While adding a data source to AlphaTrace, I ran a routine check against live data and found something I had never once doubted: the platform silently discards 92% of spot trades the moment it reads them off-chain — and says nothing. This post is about that self-correction, and about the second mistake I nearly made.

It started with someone else’s article

A few days ago I read a write-up: two people ran a cross-market arbitrage between Hyperliquid’s equity perpetuals and the traditional broker IBKR, making ten million dollars in ten months. The most valuable part wasn’t the profit — it was the post-mortem of a $1.1M loss. The broker’s market-data feed stalled, the bot concluded its two legs were misaligned, and it kept shorting to “correct” an exposure that did not exist. It ended up net short $120M of gold futures.

My reaction was immediate: could AlphaTrace recognize “this address is running cross-market arbitrage”?

AlphaTrace is a quant analysis platform I built. It doesn’t trade. It does exactly one thing: it pulls a public on-chain address’s trade history, reconstructs what the market looked like at each entry, and then lets a batch of classic strategy hypotheses (momentum, breakout, mean reversion…) compete over which best explains why those trades happened when they did. It always returns “the most likely explanation” — never “the truth,” and never “a money printer.”

Recognizing cross-market arbitrage requires seeing both markets at once. So I set out to change the data layer — but before touching anything, I did something I thought was just a formality: I verified against live data.

The check: the numbers don’t line up

I called Hyperliquid’s public API for the full fill history of the address the platform was analyzing.

It returned 493 fills.

AlphaTrace’s own database had 12 trades for the same address.

A 41x gap. My first assumption was that I’d over-fetched — wrong time range, something. So I grouped the returned data by market type:

  • Spot fills: 454, spanning 2024-04 to 2026-09
  • Perp fills: 39, spanning 2023-08 to 2024-03

Then I looked back at those 12 trades. Every one of them came from perps. Not a single one of the 454 spot fills ever made it in.

Root cause: one perfectly reasonable line

The problem lives in the function that reads on-chain fills:

1
2
if coin not in perp_coins:
    return None

It reads as harmless: if the coin isn’t in the perp list, skip it. The function was written for perps in the first place, and at the time filtering out unrelated rows felt clean.

But Hyperliquid’s spot fills look like this in the API response: @150, @107, @4. They aren’t in the perp list, so that return None swallows every one of them. No error. No warning. Just quiet nothing.

There’s a second wrinkle: @150 isn’t a coin name either — it’s the exchange’s internal index. Turning it into something readable like USDE/USDC requires a separate lookup table. I confirmed that path works: @107 resolves to HYPE/USDC, and @150 resolves to USDE/USDC.

One address: 493 fills visible vs 39 that actually reach the parser
Left: every fill the API returned. Right: the two activity windows barely overlap.

In other words, 92% of this address’s behavior was never seen by the platform. And when it delivered conclusions, it spoke with the same confidence as always.

Mistake two: I almost published an inference as fact

The fix was clear enough: add two fields to distinguish “spot vs perp” and “which exchange,” then ingest spot fills. I wrote the design doc, and slipped in a line that flattered the work — because both legs live on Hyperliquid, the claim “this address operates in two markets” could be treated as fact-level evidence, stronger than the original article’s setup where the second leg was at a broker (unobservable).

Then I re-checked the data, and that line was wrong.

Perp activity clusters from 2023-08 to 2024-03. Spot activity clusters from 2024-04 to 2026-09. There’s a one-month gap, and the two windows barely overlap.

This address participated in two markets sequentially — not holding both legs simultaneously. It is not the kind of cross-market arbitrageur I was looking for at all.

I deleted the line, and wrote a standalone correction into the doc marking that the next phase’s data validation cannot rely on this address and must find a sample with genuinely overlapping legs.

The luck here was in the timing: I re-checked after writing, rather than after publishing a conclusion. Had that line stayed, every downstream inference would have rested on a sample that doesn’t exist — and that is more dangerous than losing 92% of the data. Lost data is at least quiet. A fabricated causal claim contaminates everything downstream of it.

What this fix is actually worth

I assumed this was groundwork: add fields, ingest spot, write the arbitrage hypothesis later.

The live check changed what it is. This isn’t groundwork for a future feature — it’s repairing a data-loss bug that is actively corrupting every analysis conclusion.

Every prior analysis of this address ran on 8% of the data. The sample was pitifully small — which explains why, in the earlier hypothesis competition, the top-ranked strategy rested on just 3 valid data points. At the time I assumed the data source simply had no data. Now I know: I dropped it at my own doorstep.

That forces an uncomfortable corollary: the historical conclusions need to be re-run. After the fix, the same address should feed roughly 493 fills into the analysis instead of 39. The rankings will likely shift, and some “who won” conclusions may be overturned.

That sounds like bad news, but it’s the most valuable part of this fix — a fix that overturns its own past conclusions is one that actually fixed something.

Four things worth taking away

One: return None is the most dangerous kind of filter. It doesn’t raise, doesn’t log, doesn’t warn — it just makes data vanish. Had I written raise, or at minimum a log.warning, this wouldn’t have stayed hidden this long. Filtering out “irrelevant” data isn’t the sin; letting it happen silently is.

Two: check live data before writing the design. My first design doc was written from reading code; the second from measurements. They reached different conclusions. Code tells you “how it was written”; live data tells you “what it actually does.” Those are frequently not the same thing.

Three: after you write down an inference, check it once more. I nearly left a claim in the doc that the address held both legs simultaneously. It wasn’t a lie — it was an inference that flowed too smoothly, because I was looking for a cross-market arbitrage example and here was an address with both spot and perp records. Assuming they overlapped was the path of least resistance. Re-checking took under a minute.

Four: distinguish “unseen” from “nonexistent.” The 92% of spot activity was, inside the platform, indistinguishable from activity that never happened. In any analysis system, silently filtered data and absent data produce identical conclusions — unless you go out of your way to check the raw source.

And one last thing I keep turning over: if I hadn’t planned to change the data layer, I would never have checked this data. I found it only because I was about to touch that code and verified on the way in.

What about the places I have no plans to touch?