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