Why byte-exact decimals matter for backtests
Every price, size, fee, and PnL in our pipeline is a decimal string, end to end, never a float. The absolute error floats introduce is tiny; the damage is categorical: a ledger that can no longer be reconciled by equality. Here is where the drift comes from, what it does at 6.5M fills a day, and the reconciliation discipline exactness makes possible.
HyperliquidRPC Research
Engineering & market structure
There is a moment in every backtesting stack's life when a wallet's computed PnL disagrees with the chain by $0.000000000000018 and an engineer spends an afternoon discovering that nothing is wrong: the data was simply parsed into float64 somewhere on its way in. This post is the long-form version of that afternoon: where float drift comes from, what it costs at Hyperliquid's volume, and why our pipeline preserves raw financial values as decimal strings from the node through storage and manual raw exports.
Where drift comes from
Float64 cannot represent most decimal fractions. The number 0.1 does not exist in binary floating point; the nearest representable value is used instead, and every arithmetic step rounds again to the nearest representable result. Each individual error is on the order of one part in 10^16: negligible in isolation, and precisely why it survives code review. Four concrete cases:
A backtest is a machine for doing the last row millions of times. Every fill multiplies a price by a size, applies a fee, and adds the result to a running position and a running PnL. At the venue's current pace that is 6.5M+ trades a day, each one an opportunity for the binary ledger to step a few ulps further from the decimal truth.
What drift looks like at 6.5M fills
We simulated a single day's ledger at venue volume (realistic price and size distributions, fees applied per fill), once with exact decimal arithmetic and once with float64, and tracked the absolute divergence of the two running totals.
The honest reading of that chart: the money is irrelevant. Four cents of drift on ~$10.5B of daily notional will never change a Sharpe ratio. The damage is categorical, not financial, with three failures that have nothing to do with the drift's size:
- Reconciliation by equality becomes impossible. Once ledgers drift, you compare with tolerances; once you have tolerances, real data bugs that fall inside the band (a missed fill, a double-counted rebate) become invisible. The epsilon you added to silence float noise is now hiding actual errors.
- Results stop being reproducible. Float summation is order-sensitive ((a+b)+c ≠ a+(b+c)), so re-running the same backtest with different parallelism or sort order produces a different ledger. Byte-exact inputs with decimal arithmetic produce one answer.
- Boundary behavior is wrong. Tick rounding, trigger comparisons, and liquidation thresholds are exact-boundary operations; the 2.675 row above is a one-tick error from representation alone, before any accumulation.
What the pipeline does instead
The rule is simple to state and tedious to honor: raw financial values must not become binary floats in the ingestion path. The node emits decimal strings; ingestion parses them as decimals and storage uses decimal-typed columns sized for Hyperliquid's price and size ranges. Manual raw exports serialize those financial values back to strings, matching the live gRPC fields. Calculated or aggregated outputs are documented separately with each custom delivery.
Tedious is the operative word: the discipline mostly consists of refusing convenient defaults. JSON parsers want to give you doubles. ORMs want to map to float columns. Dataframe libraries want float64 for vectorization. Each is individually reasonable and collectively how byte-exactness dies. We treat any float64 in the financial path as a build failure.
Reconciliation, the payoff
Exact decimal storage enables equality-based reconciliation for captured raw fills rather than tolerance-based comparison. For a delivered range, sum signed closed PnL and fees, compare them with the same raw fills from an independently synced node, and investigate any nonzero difference. This applies to stored raw financial values; calculated or aggregated outputs have their own documented precision.
That equality is also your acceptance test. When you ingest our data, run the same assertion against your own ledger; if your stack preserves the strings, it passes exactly. The first time it fails, you have found the float in your pipeline, usually inside a JSON parser you did not write.
Keeping it exact in your stack
The client-side rules fit in a code comment: parse financial strings into a decimal type (or integer ticks), do arithmetic there, and convert to float only at the final, presentational step (plotting, summary stats), where binary rounding can no longer propagate.
from decimal import Decimal # Every financial field arrives as a string. Keep it that way.fill = {"px": "2.675", "sz": "1240.5", "closed_pnl": "-13.3675"} notional = Decimal(fill["px"]) * Decimal(fill["sz"]) # Decimal('3318.3375') - exactnaive = float(fill["px"]) * float(fill["sz"]) # 3318.337499999... - already off ledger = Decimal("0")ledger += Decimal(fill["closed_pnl"]) # reconciles to the chain by ==If you take one sentence away: the cost of floats in market data is not the error, it is the loss of the ability to know whether you have one. Byte-exact decimals keep equality on the table, and equality is the only reconciliation primitive that never lies.