Skip to content

Use BigDecimal for monetary fields (drop Double for money) #52

Description

@MarketDataApp

Summary

sdk-java v1.0.0 does not satisfy SDK Requirements §11.5 (Monetary Value Precision). Java is a Must language under that requirement — it has java.math.BigDecimal — but every monetary field in the SDK is currently Double, and precision is additionally destroyed at the JSON parse boundary.

Breaking change → v2.0.0 (public record component types change).

Why this matters

Binary floats cannot represent decimal money exactly (0.1 + 0.2 != 0.3). Any comparison, rounding, or accumulation a consumer performs on SDK output drifts. Prices are the primary product of this API; they must round-trip exactly.

Root cause: precision dies before the model exists

This is not a find-and-replace on field types. Three things conspire:

  1. JsonResponseParser builds a bare new ObjectMapper()DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS is not enabled, so JSON floats decode into the tree as DoubleNode.
  2. ParallelArrays extracts cells via cell.asDouble().
  3. The record components are declared Double.

Because (1) happens first, changing only (3) would not fix anything: JsonNode.decimalValue() on a DoubleNode returns BigDecimal.valueOf(double), which faithfully preserves the already-wrong value. All three must land together.

Scope: fields to convert to BigDecimal

File Fields
stocks/StockQuote.java ask, bid, mid, last, change, open, high, low, close, week52High, week52Low
stocks/StockCandle.java open, high, low, close
stocks/StockPrice.java mid, change
stocks/StockEarning.java reportedEPS, estimatedEPS, surpriseEPS
funds/FundCandle.java open, high, low, close
options/OptionQuote.java strike, bid, mid, ask, last, intrinsicValue, extrinsicValue, underlyingPrice

options/OptionsChain.java needs no change — it reuses OptionQuote rows.

Already correct — do not change

The SDK's non-monetary typing is already right and is explicitly in-spec:

  • Ratios / statistics stay Double: changepct, surpriseEPSpct, iv, delta, gamma, theta, vega, rho
  • Sizes / counts stay Long/Integer: bidSize, askSize, volume, openInterest, dte, fiscalYear, fiscalQuarter

Proposed change set

  1. Enable DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS on the ObjectMapper in JsonResponseParser so the tree carries DecimalNode.
  2. Change ParallelArrays.dbl(...) / dblOrNull(...) to return BigDecimal via cell.decimalValue(), renaming to dec/decOrNull. Keep the existing isNumber() guard and the null/NaN → null semantics unchanged.
  3. Flip the record components in the six files above to @Nullable BigDecimal.
  4. Update tests: ~14 Double references across 8 test files. Add assertions that a monetary value with more precision than a double can hold survives decode exactly — that is the regression this issue exists to prevent.
  5. Update README/Javadoc to state that monetary fields are BigDecimal and non-monetary numerics remain Double/Long.

Open question — request-side parameters

options/OptionsChainRequest.java has ~21 Double filter params, some of which are money (strike bounds). §11.5 governs typed models, so request params are arguably out of scope. Flagging for an explicit decision rather than silently leaving them — either answer is defensible, but it should be recorded.

Migration notes for v2.0.0

  • Callers doing arithmetic on monetary fields must switch to BigDecimal operations (no mixing with double in one expression).
  • Equality: BigDecimal.equals is scale-sensitive (1.50 != 1.5); use compareTo for value equality. Worth calling out in the migration guide, since record equals will inherit this.
  • Non-monetary fields are unchanged, so Greeks/IV/percentage/size consumers are unaffected.

Reference

SDK Requirements §11.5 — Monetary Value Precision. Mirrors the Python decision in MarketDataApp/sdk-py#50.

Metadata

Metadata

Assignees

No one assigned

    Labels

    breaking-changeBreaks backward compatibility; requires a major version bumpenhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions