Quant developer HFT course · Module 10: Numbers and time
Prices as integer ticks
The question
How would you represent prices in a trading system? What are the pitfalls of your choice?
The representation
Every exchange-traded instrument has a minimum price increment, its tick size: 0.25 index points for an equity index future, a cent or less for many shares, 1/64 or 1/128 of a point for some bond futures. Every valid price is a whole number of ticks. So store the price as that whole number, in a 64-bit integer:
using Ticks = std::int64_t; // or a strong type, below
struct InstrumentSpec {
std::int64_t tick_num, tick_den; // tick size as a fraction, e.g. 1/4 for 0.25
std::int64_t multiplier; // contract value per point
};
Ticks to_ticks(std::int64_t px_scaled, std::int64_t scale, const InstrumentSpec& s); // at the edge
Why integers
- Exact. 17285 ticks is exactly 4321.25; there is no rounding in storing or adding.
- Exact comparison. Equality and ordering are exact, so prices can be hash keys and array indices, and a computed price matches the book's level exactly.
- Fast. Integer arithmetic and comparison are single instructions, and tick offsets index arrays directly (Module 9).
- Matches the exchange. Exchange protocols send prices as scaled integers, so the conversion is often a multiplication or nothing at all.
An alternative seen in many codebases is a fixed number of implied decimal places for all instruments, for example prices stored as integers times 10⁹. It is also exact, simpler across instruments, and requires checking that every tick size divides evenly at that scale.
Where conversions happen
Only at the edges:
- In: from the exchange's scaled integer or decimal text into ticks, once, in the feed handler or the order decoder. Check that the price really is on a tick, and reject or flag it if not.
- Out: from ticks to the exchange's wire format when encoding an order.
- For people: to a decimal string for logs and screens, off the hot path.
A strategy that computes a fair value as a double must round it to a tick explicitly, and the rounding direction is a trading decision: a buy price rounds down to stay at or below the value, a sell price rounds up.
Ticks buy_price(double fair, double tick) { return static_cast<Ticks>(std::floor(fair / tick)); }
Ticks sell_price(double fair, double tick) { return static_cast<Ticks>(std::ceil(fair / tick)); }
(For values exactly on a tick, fair / tick may come out as 17284.999999999998; adding a small epsilon in the right direction, or computing fair values in ticks from the start, avoids an off-by-one tick.)
The pitfalls
Overflow. Notional value is price times quantity times multiplier. In ticks, with 64-bit integers that is usually ample, but it is easy to compute it in 32 bits by accident, or to scale prices by 10⁹ and multiply by a large quantity and overflow 2⁶³. Size the intermediate type for the product, use 128-bit intermediates (__int128) where needed, and test with extreme values.
Mixing units. A price in ticks of one instrument added to a price in ticks of another, or compared with a price in implied decimals, is a silent bug. A strong type that carries its meaning prevents it at compile time:
struct Price { std::int64_t ticks; };
constexpr bool operator<(Price a, Price b) { return a.ticks < b.ticks; }
constexpr Price operator+(Price a, std::int64_t n) { return {a.ticks + n}; } // move by n ticks
// no implicit conversion to or from double, no Price + Price
Division. Averages and ratios leave the integers. An average fill price is a total of price times quantity divided by total quantity; keep numerator and denominator as integers as long as possible, and convert to a double deliberately when a real number is what you want.
Tick size changes. Some venues have tick sizes that depend on the price level (tick-size regimes in European equities, for example) or that change by instrument over time. The specification must come from reference data per instrument, not a constant.
Negative prices. Spreads, some energy futures, and calendar spreads can trade at negative prices. A signed type handles it; code that assumes a price is positive does not.
The answer
Integers counting ticks, 64-bit, with the tick size and multiplier per instrument from reference data, converted once at the edges and wrapped in a strong type so units cannot be mixed. Everything internal is exact and fast. The pitfalls are overflow in notional calculations, division for averages, rounding computed fair values to a tick in the right direction, and instruments with variable tick sizes or negative prices.
Check yourself
Question 1 of 3
The tick size is 0.25. What is the integer representation of 4321.75?
4321.75 / 0.25 = 17287. Tick counts depend on the instrument's tick size, which must come from reference data.
Question 2 of 3
A fair value of 100.13 must be turned into a buy order price with a 0.05 tick. Which price, in ticks, is appropriate?
100.13 / 0.05 = 2002.6. A buy should not pay more than the fair value, so it rounds down to 2002; a sell would round up to 2003.
Question 3 of 3
Which mistake does a Price strong type prevent?
The type system rejects operations that make no sense for prices. Overflow still needs wide enough types and tests.
In the interview
Say your own answer out loud first, then open the one we would give.
How do you represent prices?
As 64-bit integers counting ticks of the instrument, with tick size and multiplier from reference data, or as integers with a fixed number of implied decimals across the system. Conversion happens once at the edges; inside, everything is exact integer arithmetic, so prices compare exactly and work as keys and indices.
What can go wrong with integer prices?
Overflow when multiplying by quantity and multiplier, especially with high implied-decimal scales; averages and ratios that need division; rounding computed fair values to a tick in the right direction; mixing units across instruments, which a strong type prevents; tick sizes that vary by price level; and negative prices.
Why not just use double?
Most decimal prices are not exactly representable in binary, so comparisons with book levels, use as hash keys and long sums go wrong in rare cases. Integers are exact, faster and match the exchange's own representation. Doubles stay for genuinely continuous quantities like fair values and volatilities.