Stage 2 of 4, lesson 10 of 31. Beginner
Handle money with Decimal, not float
See where ordinary decimal numbers go slightly wrong, and use Decimal to add up and round money exactly. It introduces the decimal module, quantize and choosing a rounding rule.
Risk warning: CFDs and spread bets are complex instruments and come with a high risk of losing money rapidly due to leverage. You can lose money on these products, and each provider publishes its own loss figure under FCA rules. You should consider whether you understand how they work and whether you can afford to take the high risk of losing your money. Automated and API trading can amplify losses through bugs, connectivity failures and runaway orders.
You need: The lessons on variables and types and on reading prices from a file. No broker account.
The float type stores numbers in binary, and most decimal fractions have no exact binary form. One tenth is stored as something very close to one tenth. Each sum is out by a hair, and the hairs add up.
For a chart that does not matter. For money it does, because money has to reconcile to the penny against a statement, and a total that is out by a tiny fraction can fail an equality test or round the wrong way.
Decimal stores decimal numbers exactly, the way you would write them on paper. Two rules go with it. Build a Decimal from text, never from a float, or the float's error comes in with it. And round on purpose, at the end, with a rule you chose, using quantize.
The script shows each failure on a float and the same sum done with Decimal, then totals the sample trades both ways. The trades are invented.
Files for this lesson
- sample-trades.csv, sample data with invented values
The code
# Handle money with Decimal instead of float.
# Uses sample-trades.csv from the trade journal lesson. The trades are invented.
import csv
from decimal import Decimal, ROUND_HALF_UP
# A float stores most decimal fractions slightly wrong. Usually it does not show. Sometimes it does.
print("float: 0.1 + 0.2 =", 0.1 + 0.2)
print("Decimal: 0.1 + 0.2 =", Decimal("0.1") + Decimal("0.2"))
# The same charge added ten times.
float_total = 0.0
decimal_total = Decimal("0")
for _ in range(10):
float_total += 0.1
decimal_total += Decimal("0.1")
print("Ten charges of 0.1 as float:", float_total, "equal to 1.0?", float_total == 1.0)
print("Ten charges of 0.1 as Decimal:", decimal_total, "equal to 1.0?", decimal_total == Decimal("1.0"))
# Build a Decimal from text, never from a float: the float brings its error with it.
print("Decimal(0.1):", Decimal(0.1))
print("Decimal('0.1'):", Decimal("0.1"))
# Round money on purpose, to the penny, with a rule you chose.
PENNY = Decimal("0.01")
print("round(2.675, 2) on a float:", round(2.675, 2))
print("Decimal 2.675 to the penny:", Decimal("2.675").quantize(PENNY, rounding=ROUND_HALF_UP))
# Total the sample trades both ways, without rounding along the way.
float_sum = 0.0
decimal_sum = Decimal("0")
with open("sample-trades.csv", newline="") as f:
for row in csv.DictReader(f):
sign = 1 if row["direction"] == "BUY" else -1
float_sum += sign * (float(row["exit"]) - float(row["entry"])) * float(row["size"])
decimal_sum += sign * (Decimal(row["exit"]) - Decimal(row["entry"])) * Decimal(row["size"])
print("Total as float:", float_sum)
print("Total as Decimal:", decimal_sum)
print("To the penny:", decimal_sum.quantize(PENNY, rounding=ROUND_HALF_UP))What you should see
float: 0.1 + 0.2 = 0.30000000000000004
Decimal: 0.1 + 0.2 = 0.3
Ten charges of 0.1 as float: 0.9999999999999999 equal to 1.0? False
Ten charges of 0.1 as Decimal: 1.0 equal to 1.0? True
Decimal(0.1): 0.1000000000000000055511151231257827021181583404541015625
Decimal('0.1'): 0.1
round(2.675, 2) on a float: 2.67
Decimal 2.675 to the penny: 2.68
Total as float: 9.799999999999756
Total as Decimal: 9.8
To the penny: 9.80Run with Python 3.12 on Windows against the sample files on this page. No network and no account.
Check yourself
Answer each question from the code above before you open it. The answers were written from the script on this page and what it printed.
Why does Decimal(0.1) print a number more than fifty digits long?
0.1 written without quotes is already a float, and that long number is what the float really holds. Decimal copies it faithfully. Decimal("0.1"), built from text, is exactly one tenth.
What does the total of the sample trades come to as a float and as a Decimal?
The float total prints as 9.799999999999756 and the Decimal total as 9.8. The trades are the same. Only the type differs.
Why does round(2.675, 2) give 2.67 when the Decimal line gives 2.68?
The float nearest to 2.675 is slightly below it, so it rounds down. The Decimal is exactly 2.675, and quantize with ROUND_HALF_UP takes a half upwards, giving 2.68.
Exercise
Add up a hundred charges of 0.1 instead of ten, and compare the totals with 10.0. Change the loop, both comparisons and both labels.
What the changed script prints
float: 0.1 + 0.2 = 0.30000000000000004
Decimal: 0.1 + 0.2 = 0.3
A hundred charges of 0.1 as float: 9.99999999999998 equal to 10.0? False
A hundred charges of 0.1 as Decimal: 10.0 equal to 10.0? True
Decimal(0.1): 0.1000000000000000055511151231257827021181583404541015625
Decimal('0.1'): 0.1
round(2.675, 2) on a float: 2.67
Decimal 2.675 to the penny: 2.68
Total as float: 9.799999999999756
Total as Decimal: 9.8
To the penny: 9.80Captured from a run of the solution on . Run with Python 3.12 on Windows against the sample files on this page. No network and no account.
One solution
# Exercise solution: the lesson script with the one change the exercise asks for.
# Handle money with Decimal instead of float.
# Uses sample-trades.csv from the trade journal lesson. The trades are invented.
import csv
from decimal import Decimal, ROUND_HALF_UP
# A float stores most decimal fractions slightly wrong. Usually it does not show. Sometimes it does.
print("float: 0.1 + 0.2 =", 0.1 + 0.2)
print("Decimal: 0.1 + 0.2 =", Decimal("0.1") + Decimal("0.2"))
# The same charge added a hundred times.
float_total = 0.0
decimal_total = Decimal("0")
for _ in range(100):
float_total += 0.1
decimal_total += Decimal("0.1")
print("A hundred charges of 0.1 as float:", float_total, "equal to 10.0?", float_total == 10.0)
print("A hundred charges of 0.1 as Decimal:", decimal_total, "equal to 10.0?", decimal_total == Decimal("10.0"))
# Build a Decimal from text, never from a float: the float brings its error with it.
print("Decimal(0.1):", Decimal(0.1))
print("Decimal('0.1'):", Decimal("0.1"))
# Round money on purpose, to the penny, with a rule you chose.
PENNY = Decimal("0.01")
print("round(2.675, 2) on a float:", round(2.675, 2))
print("Decimal 2.675 to the penny:", Decimal("2.675").quantize(PENNY, rounding=ROUND_HALF_UP))
# Total the sample trades both ways, without rounding along the way.
float_sum = 0.0
decimal_sum = Decimal("0")
with open("sample-trades.csv", newline="") as f:
for row in csv.DictReader(f):
sign = 1 if row["direction"] == "BUY" else -1
float_sum += sign * (float(row["exit"]) - float(row["entry"])) * float(row["size"])
decimal_sum += sign * (Decimal(row["exit"]) - Decimal(row["entry"])) * Decimal(row["size"])
print("Total as float:", float_sum)
print("Total as Decimal:", decimal_sum)
print("To the penny:", decimal_sum.quantize(PENNY, rounding=ROUND_HALF_UP))Your progress
Your marks are kept in this browser only, in its local storage. They are not sent anywhere, there is no account, and they will not follow you to another browser or device. To remove them, use Clear my progress on the learning track page, or clear this site's data in your browser's settings. Marking needs JavaScript. With scripts off the track reads the same, without the ticks.
This browser refused to store anything for this page, so marks cannot be kept here.
Where this sits in the path
Stage 2 of 4: Working with data. Read prices and trades from files, get dates and money right, and turn a list of trades into a journal.
- Prices from a fileDone
- Dates and time zonesDone
- Read and write JSONDone
- Money with DecimalDoneYou are here
- Build a trade journalDone
- Measure drawdownDone