---
title: "0.3.1 Python variables, types, operators and decimal"
description: "Python variables, types and operators with prices and orders; why 0.1 + 0.2 is not 0.3 and how to round to the tick with decimal."
url: "https://pythonparatrading.com/en/python-trading-course/level-0/python-variables-types-and-decimal/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-0/python-variables-types-and-decimal.png"
---

# 0.3.1 · Variables, types and operators; floating point and decimal for prices and ticks

> A price, a quantity and a ticker are data of different types, and Python treats them differently. The lesson introduces the basic types and their operators, and the first classic error of trading in code: floating point.

- Course: [Level 0: Python from scratch and a data mindset](https://pythonparatrading.com/en/python-trading-course/level-0/)
- Module: 0.3 · Language fundamentals
- Duration: 45 min
- Prerequisites: [0.2.3 · Editors (VS Code, Cursor, PyCharm) and notebooks (Jupyter, JupyterLab, marimo)](https://pythonparatrading.com/en/python-trading-course/level-0/code-editors-jupyter-marimo-notebooks/)

## By the end of this lesson

- Create variables of the types `int`, `float`, `str` and `bool` and check their type with `type()`.
- Compute the value of a position, its P&L and the full lots of an order with the arithmetic operators, `//` and `%`.
- Explain why `0.1 + 0.2 == 0.3` is `False` and compare floating-point prices without falling into that error.
- Round a price to a market's tick with `Decimal` and `quantize`, choosing the rounding mode according to the side of the order.

## Variables: names for market data

A program works with data: the price of an asset, the quantity of an order, the symbol being traded. A **variable** is
a name that points to one of those pieces of data. It is created with the `=` sign, which in Python does not mean
“equal” but “assign the value on the right to this name”:

```python
ticker = "SINT"
precio = 142.22
cantidad = 100
es_compra = True

print(ticker, precio, cantidad, es_compra)
print(type(ticker), type(precio), type(cantidad), type(es_compra))
```
```output
SINT 142.22 100 True
<class 'str'> <class 'float'> <class 'int'> <class 'bool'>
```

Every value has a **type**, and `type()` tells you which. The four in the cell are the basic types that appear in any
trading program:

| Type | What it holds | Trading example | Python literal |
|---|---|---|---|
| `int` | Integers, with no size limit | Shares in an order, contracts, volume | `100`, `-3`, `2_322_609` |
| `float` | Numbers with decimals (binary floating point) | Prices, returns, volatility | `142.22`, `-0.035`, `1e-4` |
| `str` | Text | Ticker, market, side of the order | `"SINT"`, `"buy"` |
| `bool` | True or false | Is there an open position? Was the stop hit? | `True`, `False` |
| `NoneType` | The absence of a value | A stop that has not been set yet | `None` |

Three rules about names. They are written in lowercase with underscores (`precio_entrada`, not `PrecioEntrada`): it is
the Python convention and the one the whole course follows. They cannot start with a number or match reserved words
(`if`, `for`, `class`…). And they should say what they hold: `p` or `x2` force you to read the rest of the code to know
what they are; `precio_cierre` does not.

In code, the decimal separator is the point (`142.22`), not the comma. An underscore inside a number only makes it
easier to read: `2_322_609` is the same `int` as `2322609`.

A variable can be reassigned, and from that moment on it points to the new value. The type belongs to the value, not to
the name:

```python
stop = None          # no stop yet
stop = 138.50        # now it is a float
print(stop, type(stop))
```
```output
138.5 <class 'float'>
```

Python shows `138.5`, not `138.50`: a `float` does not store how many decimals were written, only the number. How to
always show two decimals is covered in lesson 0.3.2.

## Arithmetic operators

Operators combine values. The arithmetic ones are those of a calculator, plus three that are used daily in trading:
integer division `//`, the remainder `%` and the power `**`.

| Operator | Operation | Example | Result |
|---|---|---|---|
| `+` `-` `*` | Addition, subtraction, multiplication | `142.22 * 100` | `14222.0` |
| `/` | Division (always `float`) | `10 / 2` | `5.0` |
| `//` | Integer division (rounded down) | `1250 // 100` | `12` |
| `%` | Remainder of integer division | `1250 % 100` | `50` |
| `**` | Power | `1.05 ** 10` | `1.628…` |

The value of a position, its result and its return come from multiplying, subtracting and dividing:

```python
precio_entrada = 135.40
precio_actual = 142.22
cantidad = 100

valor = precio_actual * cantidad
pnl = (precio_actual - precio_entrada) * cantidad
rentabilidad = precio_actual / precio_entrada - 1

print(valor)
print(pnl)
print(rentabilidad)
```
```output
14222.0
681.9999999999993
0.050369276218611425
```

The output already hints at the central topic of the lesson: the P&L, which should be exactly €682, shows up with a
tail of decimals. Before explaining it, the other two operators.

Integer division and the remainder solve a problem that appears as soon as you compute the size of an order: many
orders only accept multiples of a lot, and futures are traded in whole contracts. `//` gives how many fit and `%` what
is left over:

```python
acciones = 1_250
lote = 100
print("Full lots:", acciones // lote)
print("Odd shares:", acciones % lote)

capital = 25_000
garantia_contrato = 6_000
print("Contracts:", capital // garantia_contrato)
print("Unused capital:", capital % garantia_contrato)

print("Capital at 5% a year after 10 years:", 10_000 * 1.05 ** 10)
```
```output
Full lots: 12
Odd shares: 50
Contracts: 4
Unused capital: 1000
Capital at 5% a year after 10 years: 16288.94626777442
```

Two details of this cell. `print` accepts several values separated by commas and writes them with a space between
them. And the order of operations is the mathematical one: first `**`, then `*`, `/`, `//` and `%`, and finally `+`
and `-`. That is why `10_000 * 1.05 ** 10` raises to the power before multiplying. When in doubt, parentheses.

There is a difference between `/` and `//` worth settling from the start: `/` always returns a `float`, even when the
division is exact (`10 / 2` is `5.0`). If the result is going to count shares or contracts, it has to come from `//`
or from an explicit rounding, never from simply turning a `float` into an `int` (the reason is shown below).

## Comparisons and logic

The comparison operators (`==`, `!=`, `<`, `<=`, `>`, `>=`) return a `bool`, and `and`, `or` and `not` combine them.
They are the raw material of any trading rule:

```python
cierre = 142.22
media_200 = 131.80
stop = 138.50

por_encima = cierre > media_200
stop_tocado = cierre <= stop
mantener = por_encima and not stop_tocado
print(por_encima, stop_tocado, mantener)
```
```output
True False True
```

Watch the difference between `=` (assign) and `==` (compare). What to do with these conditions (buy, sell, do nothing)
is the subject of lesson 0.3.4.

## Floating point: why 0.1 + 0.2 is not 0.3

A `float` is stored in **binary**, with 53 bits of precision (the IEEE 754 standard, the same on every computer). Just
as 1/3 has no finite decimal expansion (0.3333…), most decimal fractions have no finite binary expansion: in base 2,
0.1 is a repeating fraction. The computer stores the nearest binary number, and the difference shows up as soon as you
operate:

```python
from decimal import Decimal

print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(Decimal(0.1))
print(Decimal(142.22))
```
```output
0.30000000000000004
False
0.1000000000000000055511151231257827021181583404541015625
142.219999999999998863131622783839702606201171875
```

`Decimal(0.1)` shows the exact value held in memory when you write `0.1`: almost, but not exactly, one tenth. The same
happens with almost any price with decimals. `print` hides it because it shows the shortest number that gives back the
same `float`, but the stored value is the one on the last line.

In trading, this representation error causes three typical failures:

- **Comparing with `==`**. A computed price (`entrada + 3 * tick`) may not equal the price that comes from the market
  even though both read the same. An `if precio == objetivo` condition that is never met is an error that is hard to
  see.
- **Counting ticks with `int()`**. `int` does not round: it cuts off the decimals. If the division gives
  9.99999999999943…, the result is 9.
- **Rounding to the cent with `round()`**. `round(2.675, 2)` gives `2.67`, because 2.675 is stored as 2.67499999…;
  besides, `round` rounds ties to even (`round(0.125, 2)` is `0.12`).

```python
tick = 0.01
print((101.03 - 101.00) / tick, int((101.03 - 101.00) / tick))
print((100.05 - 99.95) / tick, int((100.05 - 99.95) / tick))
print(round(2.675, 2), round(0.125, 2))

tick_futuro = 0.25
print((4500.75 - 4500.00) / tick_futuro)
```
```output
3.0000000000001137 3
9.999999999999432 9
2.67 0.12
3.0
```

Between 99.95 and 100.05 there are ten ticks of 0.01; `int` counts nine. The last line, on the other hand, is exact:
0.25 is 1/4, a finite binary fraction, so prices in quarters are represented without error. Ticks of 0.01, 0.05 or
0.1 are not.

To compare floating-point prices, the rule is to compare with a tolerance (`math.isclose(a, b)` or
`abs(a - b) < tick / 2`) and, to count ticks, to round before converting: `round((b - a) / tick)`.

<aside class="callout" data-kind="aviso">

It is not a Python bug: JavaScript, C, Java and NumPy use the same floating point (IEEE 754) and give the same
results; Excel uses it too, although it rounds what it displays to 15 digits. The difference is that Python offers an
exact decimal type without installing anything.

</aside>

## How much the error accumulates

The error of each operation is on the order of 10⁻¹⁶ times the value. What matters is whether it grows when many
operations are added up, as in a trade ledger or in the P&L of a long backtest. The cell adds €0.10 of profit over and
over and measures the difference from the exact result (the `for` loop repeats the addition; it is explained in lesson
0.3.4):

```python
from decimal import Decimal

for n in [10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000]:
    total = 0.0
    for _ in range(n):
        total += 0.1
    exacto = Decimal(n) / 10
    error = abs(Decimal(total) - exacto)
    print(f"{n:>10,}  {total!r:<20}  error {float(error):.1e}")
```
```output
        10  0.9999999999999999    error 1.1e-16
       100  9.99999999999998      error 2.0e-14
     1,000  99.9999999999986      error 1.4e-12
    10,000  1000.0000000001588    error 1.6e-10
   100,000  10000.000000018848    error 1.9e-08
 1,000,000  100000.00000133288    error 1.3e-06
10,000,000  999999.9998389754     error 1.6e-04
```

`total += 0.1` is a short way of writing `total = total + 0.1`. The header figure turns those errors into **exact
decimals**: how many decimals of the total match the exact sum. After ten additions, fifteen; after ten million,
three. Two conclusions, both important:

- For statistics, returns and backtests, `float` is enough. An error in the fourth decimal after ten million additions
  does not change any decision, and NumPy and pandas (module 0.5) work in `float` for speed.
- For order prices, account balances and reconciliation with the broker, it is not enough. There the problem is not
  the size of the error but that it exists at all: a price that does not fall on the tick grid or a balance that does
  not reconcile to the cent is an error, however small.

## Decimal: exact prices and rounding to the tick

The standard library's `decimal` module represents numbers in base 10, as in accounting. A `Decimal` created **from a
string** stores exactly what is written:

```python
from decimal import Decimal

print(Decimal("0.1") + Decimal("0.2") == Decimal("0.3"))
print(Decimal("0.1"), Decimal(0.1) == Decimal("0.1"))

entrada = Decimal("135.40")
salida = Decimal("142.22")
pnl = (salida - entrada) * 100
print(pnl)
print((Decimal("100.05") - Decimal("99.95")) / Decimal("0.01"))
```
```output
True
0.1 False
682.00
10
```

The P&L comes out exact, with its two decimals, and the tick count is 10. The second line shows the trap:
`Decimal(0.1)`, without quotes, copies the `float` with its error. Prices are passed to `Decimal` as text: as they
arrive from the broker or the file, or with `Decimal(str(precio))` if they are already floats.

There are two cases for bringing a price onto the tick grid. If the tick is a power of ten (0.01, 0.001), `quantize`
is enough: it rounds to the decimals of the value it receives. If it is not (0.05, 0.25), you divide by the tick, round
to an integer and multiply back. In both cases, the **rounding mode** depends on the side of the order: a buy limit is
rounded down, so as not to pay more than the computed price, and a sell limit is rounded up.

```python
from decimal import ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_EVEN

precio = Decimal("101.237")
print(precio.quantize(Decimal("0.01")))
print(precio.quantize(Decimal("0.01"), rounding=ROUND_FLOOR))

tick = Decimal("0.05")
unidades = precio / tick
compra = unidades.quantize(Decimal("1"), rounding=ROUND_FLOOR) * tick
venta = unidades.quantize(Decimal("1"), rounding=ROUND_CEILING) * tick
print("Buy limit:", compra, " Sell limit:", venta)

print(Decimal("2.675").quantize(Decimal("0.01")),
      Decimal("2.665").quantize(Decimal("0.01"),
                                rounding=ROUND_HALF_EVEN))
```
```output
101.24
101.23
Buy limit: 101.20  Sell limit: 101.25
2.68 2.66
```

Without `rounding`, `quantize` uses the default mode, `ROUND_HALF_EVEN`: the nearest and, in case of a tie, the even
one (“banker's rounding”, which does not bias totals upward). With `Decimal`, 2.675 really is a tie and rounds to
2.68; 2.665 rounds to 2.66, the even one. `ROUND_HALF_UP` is the rounding taught at school; `ROUND_FLOOR` and
`ROUND_CEILING`, down and up.

Exactness has a price. `Decimal` is slower than `float` and does not mix with it: adding a `Decimal` and a `float` is
an error, so that nobody slips the binary error into an exact account by accident. Python signals it with an exception
(exceptions are covered in lesson 0.3.6):

```python raises
Decimal("142.22") + 0.01
```
```output
TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float'
```

And the precision of `Decimal` is finite too: 28 significant digits by default. Divisions that are not exact are
rounded there, so `Decimal` does not fix 1/3; it fixes the decimals written in base 10, which are the ones prices use.

## Which type to use in each case

| Data | Type | Why |
|---|---|---|
| Shares, contracts, lots | `int` | They are integers: with `//` and `%`, never with `int()` on a `float` |
| Returns, volatility, signals | `float` | Speed; a relative error of 10⁻¹⁶ does not change any decision |
| Order prices, ticks | `Decimal` or `int` in ticks | They have to fall exactly on the market's grid |
| Balances, commissions, taxes | `Decimal` | They must reconcile to the cent with the broker's statement |
| Prices in a vectorized backtest | `float` (NumPy/pandas) | They are compared with a tolerance and rounded to the tick when the order is generated |

The second option for prices, storing the number of ticks as an `int` (10,123 ticks of 0.01 instead of 101.23), is
the one many exchange matching engines use: integers are exact and fast.

## Exercises

1. An account has €47,300 and each futures contract requires €5,500 of margin. Compute how many contracts can be
   opened, how much capital is left unused and what percentage of the account is committed as margin.

**Solution.**

```python
capital = 47_300
garantia = 5_500
contratos = capital // garantia
print("Contracts:", contratos)
print("Unused:", capital % garantia)
print("Committed:", contratos * garantia / capital)
```
```output
Contracts: 8
Unused: 3300
Committed: 0.9302325581395349
```

Eight contracts and €3,300 unused: a little over 93% of the account is held as margin. That you can does not mean you
should: position sizing is decided by risk (level 4), not by the available margin.

2. A buy price computed by a strategy is 87.4361 and the market quotes in ticks of 0.05. With `Decimal`, compute the
   buy limit price (rounded down) and the sell limit price (rounded up), and check that both fall on the grid: the
   remainder of dividing them by the tick has to be zero.

**Solution.**

```python
from decimal import Decimal, ROUND_CEILING, ROUND_FLOOR

precio = Decimal("87.4361")
tick = Decimal("0.05")
uno = Decimal("1")
compra = (precio / tick).quantize(uno, rounding=ROUND_FLOOR) * tick
venta = (precio / tick).quantize(uno, rounding=ROUND_CEILING) * tick
print(compra, venta)
print(compra % tick, venta % tick)
```
```output
87.40 87.45
0.00 0.00
```

The `%` operator also works with `Decimal`. With `float`, the same remainder would give a tiny number instead of zero
and the check would fail.

3. Count the ticks of 0.01 between 99.95 and 100.05 in three ways: with `int()` on the `float`, with `round()` on the
   `float` and with `Decimal`. Which one fails and why?

**Solution.**

```python
from decimal import Decimal

print(int((100.05 - 99.95) / 0.01))
print(round((100.05 - 99.95) / 0.01))
print((Decimal("100.05") - Decimal("99.95")) / Decimal("0.01"))
```
```output
9
10
10
```

`int()` fails: it truncates 9.99999999999943… to 9. `round()` fixes it because the error is much smaller than half a
tick, and `Decimal` gives the exact result from the start.

## Self-assessment

1. An account has €25,000 and each futures contract requires €6,000 of margin. Which expression gives the number of contracts that can be opened?

   - A. `25_000 // 6_000`
   - B. `25_000 / 6_000`
   - C. `25_000 % 6_000`
   - D. `round(25_000 / 6_000)`

   *Answer A: `25_000 // 6_000`.* `//` is integer division: 4 contracts. `/` gives 4.1666…, `%` gives the remainder (€1,000) and `round` would give 4 here, but with 5.5 contracts it would round up and open one that cannot be covered.

2. What does `0.1 + 0.2 == 0.3` return in Python?

   - A. `True`
   - B. A type error
   - C. `False`, because 0.1 and 0.2 have no exact binary representation
   - D. It depends on the operating system

   *Answer C: `False`, because 0.1 and 0.2 have no exact binary representation.* Floats are stored in binary, and 0.1 is a repeating fraction in base 2. The sum gives 0.30000000000000004. The result is the same on any machine: it is set by the IEEE 754 standard.

3. A `float` price is 10 ticks of 0.01 above another, but `int((100.05 - 99.95) / 0.01)` returns 9. Why?

   - A. Because `int` rounds to the nearest integer
   - B. Because the floating-point subtraction gives 9.99999999999943… and `int` truncates the decimals
   - C. Because 0.01 is not a valid tick
   - D. Because of an interpreter bug

   *Answer B: Because the floating-point subtraction gives 9.99999999999943… and `int` truncates the decimals.* `int()` cuts off the decimals; it does not round. With a representation error just below the integer, the tick count loses one. With `Decimal` created from strings, the subtraction is exact.

4. You have to send a buy limit order, and the computed price, 101.237, is not on the 0.05 tick grid. Which rounding is the prudent one?

   - A. Up, to 101.25, so that it fills sooner
   - B. Always to the nearest, regardless of the side
   - C. No rounding: the broker accepts it
   - D. Down, to 101.20, so as not to pay more than the computed price

   *Answer D: Down, to 101.20, so as not to pay more than the computed price.* A buy limit rounded down never pays more than computed; a sell, rounded up. If the price does not fall on the grid, the broker rejects the order.

## Key points

- Every value has a type: `int` for quantities, `float` for calculations, `str` for tickers and `bool` for conditions. `type()` shows it.
- `/` always gives a `float`; `//` and `%` give the integer part and the remainder: full lots, contracts and odd shares.
- Floats are binary: `0.1 + 0.2` is not `0.3`. They are never compared with `==` or converted with `int()` without rounding first.
- A float's error is tiny and accumulates slowly: for statistics and vectorized backtests it is enough. For order prices and accounting, `Decimal`.
- `Decimal` is created from strings, not from floats, and `quantize` rounds to the tick with the mode the side of the order calls for.

## Resources

### Research

- [Limit Order Book - Aggregated Market Liquidity](https://pythonparatrading.com/en/limit-order-book-and-market-liquidity/) · Research-0008 · 11 min

### Documentation

- [Python: “Floating-Point Arithmetic: Issues and Limitations” (official tutorial)](https://docs.python.org/3/tutorial/floatingpoint.html) · docs.python.org
- [Python: decimal module (official documentation)](https://docs.python.org/3/library/decimal.html) · docs.python.org
- [Python: built-in types, numeric operators (official documentation)](https://docs.python.org/3/library/stdtypes.html) · docs.python.org

Previous lesson: [0.2.6 · Introduction to AI coding assistants: prompts, review and tests](https://pythonparatrading.com/en/python-trading-course/level-0/ai-coding-assistants/)

Next lesson: [0.3.2 · Strings, f-strings and formatting numbers and dates](https://pythonparatrading.com/en/python-trading-course/level-0/python-strings-f-strings-formatting/)
