Cómo está construido
ADR-0005: Exact decimals by default, floats on request, currency tags checked at runtime
Estas páginas están en inglés. Una especificación en un solo idioma dice una sola cosa.
Status: accepted (2026-10-09)
Context#
Financial quantities need exact arithmetic (fees, notionals, balances).
Analytics (statistics, volatility) want binary floating point. TTT's own
engine accounts in f64 and stores prices as integer ticks. The brief asks
to evaluate Money<Currency> and to forbid implicit conversions that change
financial meaning.
Decision#
Decimalis exact: a 128-bit signed mantissa and a scale of 0..=18 decimal places. Add/subtract are exact; multiplication is exact up to 18 places and rounds half-to-even beyond; division yields at most 18 places, rounded half-to-even. Overflow and division by zero are runtime errors, never wrap-around or infinity.- A numeric literal with a decimal point is a decimal literal; it
becomes
Decimalunless its context requiresFloat. An integer literal becomesIntunless its context requiresDecimalorFloat. No other conversion is implicit:Int + Decimalis a compile error;to_decimal,to_float,roundare explicit. Floatis IEEE binary64. A result that is NaN or infinite is a runtime error (E-NUM-NONFINITE), so no NaN ever exists as a value.- Financial types are distinct nominal types over
Decimal:Price,Quantity,Money,Ratio. Dimension rules (whatPrice * Quantityis, what is refused) are a single table int0-irused by both the type checker and the IR validator. - Currency is a runtime tag in v0:
MoneyandPricecarry aCurrency; mixing two currencies is a runtime error (E-FIN-CURRENCY-MISMATCH), never a conversion.
Alternatives#
- Static
Money<C>/Price<C>. Catches mismatches at compile time, but values arriving from the host (a dataset's quote currency, an account's balance) have currencies known only at runtime, which forces either an existential type or a checked cast at every host boundary. Deferred, not rejected. - Floats everywhere (TTT's engine choice). Rejected for the language:
0.1 + 0.2 != 0.3is not acceptable for fee and balance arithmetic.
Consequences#
- Values from TTT's
f64accounting enter T0 asDecimalvia the shortest round-trip decimal of thef64; the backtest contract says so. Quantityis not tagged with its base asset in v0;Price * Quantityis trusted to refer to the same instrument. Recorded as a known gap.
Decimal ↔ f64 boundaries#
Every place a value crosses between exact decimals and binary floats, as
of 2026-10-09 (found by searching both code bases for to_f64,
from_f64_* and Float values). Nowhere else converts; a new crossing
must be added here.
Decimal → f64 always through Decimal::to_f64: the decimal's exact
text parsed by Rust, i.e. the nearest binary64, correctly rounded. It
cannot fail (every in-range decimal is a finite float); it can lose
digits beyond ~15–17 significant figures.
f64 → Decimal only through two functions, both refusing NaN and
infinities (E-NUM-NONFINITE), never substituting a number:
from_f64_rounded(x, places) — the exact binary value rounded half-even
to places (0..=18); and from_f64_shortest(x) — the shortest decimal
that reads back as exactly x (rounded to 18 places if it needs more).
| where | direction | how | why |
|---|---|---|---|
prelude to_float(Decimal) / to_float(Int) |
→ f64 | to_f64 / Rust's as f64 (exact below 2⁵³, nearest above) |
the program asks for it |
prelude decimal_from_float(x, places) |
→ Decimal | from_f64_rounded |
the program states the rounding |
std.ind, std.math (sqrt, ln, exp, pow), std.stats |
f64 only | — | statistics are Float by design; amounts never enter them implicitly |
strategy lowering to TTT's graph (t0-host/src/bridge.rs): parameter values, constants, sizing literals, Ratio → percent |
→ f64 | to_f64 |
TTT's strategy engine computes in f64 (ADR-0004); identity hashes are computed from the translated graph, so the conversion is part of what is compared |
backtest.Spec.balance → BacktestConfig::with_starting_balance |
→ f64 | to_f64 |
TTT's accounting is f64 |
backtest.Execution fees/slippage Ratio → basis points |
→ f64 | ratio × 10 000, exact, then to_f64 |
|
| backtest report amounts (PnL, fees, balances) | → Decimal | from_f64_shortest; a non-finite amount is E-HOST-FAILED |
TTT computed them in f64; the shortest form is the value the engine holds, not a rounding the language chose |
backtest.trades PnL from the run journal |
→ Decimal | from_f64_shortest; non-finite → E-HOST-FAILED |
as above |
backtest.status progress fraction |
→ Decimal (Ratio) |
from_f64_rounded(·, 4) |
a display value; 4 places stated |
research.market_state readings |
stay Float |
finite-checked, non-finite → E-HOST-FAILED |
features are statistics computed by TTT in f64 |
| candle prices and volumes, trade prices and quantities | no float: integer ticks (1/100) and units of 1e-8 become decimals of scale 2 and 8 exactly | the dataset stores integers |
Consequence for programs: an amount that went through TTT's engine (a
backtest's PnL) is exactly what the engine computed, but the engine's own
arithmetic was binary; comparing it for equality with a decimal computed
in T0 is not meaningful. Compare with a tolerance stated in the program,
or round both with decimal.round_half_up.