EngineeringApr 28, 2026 · 7 min read

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:

Operationfloat64 resultExact resultConsequence
Parse "0.1"0.1000000000000000055511151231257827…0.1The stored value is already not the value the exchange printed.
Round "2.675" to 2 places2.672.68The float sits below 2.675, so half-up rounding goes the wrong way: a one-tick error at the tick boundary.
0.1 + 0.20.300000000000000040.3Equality checks against quoted values fail; you start writing epsilons.
Sum "0.1" ten times0.99999999999999991.0Accumulation drifts, and a PnL ledger is one long accumulation.
All four behaviors are exact, reproducible properties of IEEE-754 float64; try them in any Python or JS REPL.

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.

Fig. 1: Absolute drift of a float64 running-PnL ledger versus the exact decimal ledger, by fills processed, over one simulated day at the venue's 6.5M-trade volume. Under 4 cents after 6.5M fills: small enough to ignore, large enough to break every equality check.

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.

exact_pnl.pypython
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.

Volume figures (6.5M+ trades/day, ~$10.5B daily notional) are production-pipeline figures as of 2026-06-09. The float64 cases in the table are exact IEEE-754 behavior, reproducible in any REPL. The drift simulation is an illustrative methodology walkthrough at venue-realistic volume; the reconciliation query below runs the equality check against the live archive.

Built with the captured historical data.

Queries used
Per-wallet daily PnL for equality reconciliationsql
SELECT wallet,       toDate(ts)      AS day,       sum(closed_pnl) AS pnl,   -- decimal-typed: exact       sum(fee)        AS feesFROM tradesWHERE day = '2026-06-08'GROUP BY wallet, dayORDER BY pnl DESC;
The research letter

One post a month, built from the archive.

Market-structure analysis and engineering notes, each with the queries to reproduce it. No product announcements, no schedule padding.