Ir al contenido

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#

  • Decimal is 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 Decimal unless its context requires Float. An integer literal becomes Int unless its context requires Decimal or Float. No other conversion is implicit: Int + Decimal is a compile error; to_decimal, to_float, round are explicit.
  • Float is 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 (what Price * Quantity is, what is refused) are a single table in t0-ir used by both the type checker and the IR validator.
  • Currency is a runtime tag in v0: Money and Price carry a Currency; 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.3 is not acceptable for fee and balance arithmetic.

Consequences#

  • Values from TTT's f64 accounting enter T0 as Decimal via the shortest round-trip decimal of the f64; the backtest contract says so.
  • Quantity is not tagged with its base asset in v0; Price * Quantity is 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.