Articles › Programming

Never store money in a float: why 19.99 × 100 is not 1999, and how to stay exact

Floats cannot hold 0.1 exactly, so money drifts a cent at a time. How a live trading journal keeps money exact from source to database, with Go, SQL and Python.

Intermediate 5 min read October 16, 2026

What you will learn
  • A float is binary and cannot hold 0.1 or 19.99 exactly, so sums drift a little and the drift adds up
  • Money must stay an exact decimal at every hop, from JSON to the database column. One float hop anywhere loses it
  • Round once, in one place. Ratios such as win rate can be floats

Open your browser console and type 0.1 + 0.2. The answer is 0.30000000000000004, not 0.3. It looks like a programmer joke, but in a system that counts money it is a bug that shaves a cent here and there until someone asks where the money went. This article explains why it happens and how the trading journal we run keeps money exact from the source all the way to the database.

Every example was tested with Go 1.23, PostgreSQL 16, Python 3.12 and Node.js 22.

Why a float cannot hold 0.1

Computers store floats in binary. Some fractions that look simple in decimal never end in binary, just as 1/3 never ends in decimal (0.3333…). 0.1 repeats forever in binary, so the machine keeps only the nearest value it can.

Here is what 1234.56 is really stored as:

from decimal import Decimal
print(Decimal(1234.56))
# 1234.55999999999994543031789362430572509765625

We normally never see this, because languages round when printing. Add, subtract, multiply and divide a few times, though, and the error shows:

x = 0.0
for _ in range(10):
    x += 0.1
print(x)  # 0.9999999999999999

A float does not “calculate wrong”. It calculates correctly with a number that was never the one you meant.

A common real bug: converting to cents

Many systems know floats are risky, so they convert to cents before storing, by multiplying the float by 100:

19.99 * 100              // 1998.9999999999998
Math.trunc(19.99 * 100)  // 1998  ← one cent gone
Math.round(19.99 * 100)  // 1999

If the code truncates (trunc, or a plain cast to int, which many languages do by default), a 19.99 item is recorded as 19.98. Sell a thousand a day and ten units of currency vanish daily, with no error and no alert.

Where money leaks into floats

You may never declare a float, yet money can still pass through one here:

WhereWhat happens
JSON parsingJSON.parse in JavaScript and Go’s encoding/json (when decoding into interface{}) turn numbers into floats by default
Database columnsREAL, DOUBLE PRECISION and FLOAT store floats from the start
Libraries in betweensome ORMs and converters turn decimals into floats for you
SpreadsheetsExcel computes everything in floats
The sourcesome programs, such as MT5, hold prices as double from the start

The last one cannot be avoided. What you can do is convert it to text with the correct number of decimals once, at the source, and never let it become a float again.

The principle: exact decimals at every hop

Money must travel as an exact decimal the whole way. If any single hop passes through a float, the whole line loses its precision.

Our trading journal receives account balances from the trading program every few seconds. The path of a money value looks like this:

  1. The trading program sends JSON with the balance as a number, such as "balance": 1234.56
  2. Go receives the JSON without converting to float, keeping it as json.Number, which is the text "1234.56" inside
  3. PostgreSQL stores it in a NUMERIC(20,5) column, an exact decimal with 5 places (forex prices have 5)
  4. When computing statistics, the database sums in NUMERIC and turns the result into text with ::text
  5. The API returns text, such as "net_profit": "1234.56000"
Trading program1234.56JSON"balance": 1234.56Go: json.Number"1234.56" (text)PostgreSQL NUMERIC(20,5)1234.56000API response"1234.56000"if made float641234.5599999…drifts from here
The balance stays text or an exact decimal at every hop. One float hop and the whole line loses precision

The code at each hop

Go: read JSON without a float. Declare money fields as json.Number:

type Snapshot struct {
    Balance json.Number `json:"balance"`
    Equity  json.Number `json:"equity"`
}

When decoding JSON of unknown shape, tell the decoder not to produce floats:

dec := json.NewDecoder(r.Body)
dec.UseNumber() // every number becomes json.Number, not float64

SQL: store NUMERIC, send out text.

CREATE TABLE orders (
    profit     NUMERIC(20,5) NOT NULL,
    commission NUMERIC(20,5) NOT NULL DEFAULT 0,
    swap       NUMERIC(20,5) NOT NULL DEFAULT 0
);

SELECT coalesce(sum(profit + commission + swap), 0)::text AS net_profit
FROM orders WHERE account_id = $1;

Casting to ::text makes the driver hand over a string, so nothing along the way can turn it into a float.

Python: use Decimal, and always build it from text.

from decimal import Decimal
Decimal("19.99") * 100   # Decimal('1999.00')  correct
Decimal(19.99) * 100     # Decimal('1998.999999999999843680598133')  wrong, built from a float

The common slip is building a Decimal from a float, which carries the float’s error along. Build it from text only.

Rounding: once, in one place

Exact decimals do not mean never rounding. An average profit per trade may have endless decimals. The rules:

  • Round at the end, once. Do not round at every intermediate step; rounding errors pile up just like float errors
  • Round in one place. In our system the averaging happens in the database and is cast to NUMERIC(20,5) before becoming text. No other code rounds it again
  • Agree on the rounding method. Round half up, or banker’s rounding (half to even). Some accounting rules specify one; use the same everywhere

What may be a float

Not every number must be exact. In the same system, ratios such as win rate, profit factor and drawdown are floats, because an error at the fifteenth decimal means nothing to a reader and nobody pays out based on them.

A simple line: if this number must match money in a real account, no float. If it is an indicator for spotting trends, a float is fine.

Common mistakes

  • Converting to cents by multiplying a float and truncating (the 19.99 bug above)
  • Storing money in a FLOAT column because the ORM chose it
  • Comparing money with == on values that came from floats: 0.1 + 0.2 == 0.3 is false
  • The page receives money as a string, then converts it to Number just to display it, when it could display the string as is
  • Testing only with round numbers like 100 or 50, which never show the problem. Always include values like 19.99, 0.1 and 1234.56 in tests

FAQ

Can I store whole cents (integers) instead?

Yes, and it works well for a single currency with fixed decimals. If you store prices with 5 decimal places, or currencies with different decimals, the database's decimal type such as NUMERIC is more straightforward.

What about JavaScript in the browser?

Receive money from the API as a string and display it as a string. If you truly must calculate in the page, use a decimal library. Never convert to Number and multiply.

Commands and settings were tested on sample systems. Try them on a test machine before production.