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 actually comes from, what it costs at Hyperliquid's volume, and why our pipeline carries every financial value as a byte-exact decimal string from the node to your query result.

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: a financial value is never a binary float at any hop. The node emits decimal strings; our ingestion parses them as decimals and validates against the raw payload; storage uses decimal-typed columns sized for Hyperliquid's price and size ranges; the query API and exports serialize back to strings. The bytes you receive for a price are the bytes the chain produced. The live gRPC streams carry the same string-typed fields, which is what makes the schema-parity promise real: a backtest written on the archive consumes the live feed with zero remapping, including its number handling.

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

Exactness buys you the strongest invariant a data vendor can offer: our archive reconciles to the chain by equality, not tolerance. The methodology is deliberately boring. For every wallet and every day, sum the signed closed PnL and fees across all fills in the archive; replay the same wallet's fills from an independently synced node; assert the sums are identical to the last decimal digit. Not close. Identical. Any nonzero diff is a pipeline bug by definition, and the check runs continuously as the archive grows by millions of trades a day.

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 historical API.

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.