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:
JsonResponseParser builds a bare new ObjectMapper() — DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS is not enabled, so JSON floats decode into the tree as DoubleNode.
ParallelArrays extracts cells via cell.asDouble().
- 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
- Enable
DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS on the ObjectMapper in JsonResponseParser so the tree carries DecimalNode.
- 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.
- Flip the record components in the six files above to
@Nullable BigDecimal.
- 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.
- 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.
Summary
sdk-javav1.0.0 does not satisfy SDK Requirements §11.5 (Monetary Value Precision). Java is a Must language under that requirement — it hasjava.math.BigDecimal— but every monetary field in the SDK is currentlyDouble, 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:
JsonResponseParserbuilds a barenew ObjectMapper()—DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATSis not enabled, so JSON floats decode into the tree asDoubleNode.ParallelArraysextracts cells viacell.asDouble().Double.Because (1) happens first, changing only (3) would not fix anything:
JsonNode.decimalValue()on aDoubleNodereturnsBigDecimal.valueOf(double), which faithfully preserves the already-wrong value. All three must land together.Scope: fields to convert to
BigDecimalstocks/StockQuote.javaask,bid,mid,last,change,open,high,low,close,week52High,week52Lowstocks/StockCandle.javaopen,high,low,closestocks/StockPrice.javamid,changestocks/StockEarning.javareportedEPS,estimatedEPS,surpriseEPSfunds/FundCandle.javaopen,high,low,closeoptions/OptionQuote.javastrike,bid,mid,ask,last,intrinsicValue,extrinsicValue,underlyingPriceoptions/OptionsChain.javaneeds no change — it reusesOptionQuoterows.Already correct — do not change
The SDK's non-monetary typing is already right and is explicitly in-spec:
Double:changepct,surpriseEPSpct,iv,delta,gamma,theta,vega,rhoLong/Integer:bidSize,askSize,volume,openInterest,dte,fiscalYear,fiscalQuarterProposed change set
DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATSon theObjectMapperinJsonResponseParserso the tree carriesDecimalNode.ParallelArrays.dbl(...)/dblOrNull(...)to returnBigDecimalviacell.decimalValue(), renaming todec/decOrNull. Keep the existingisNumber()guard and the null/NaN → nullsemantics unchanged.@Nullable BigDecimal.Doublereferences across 8 test files. Add assertions that a monetary value with more precision than adoublecan hold survives decode exactly — that is the regression this issue exists to prevent.BigDecimaland non-monetary numerics remainDouble/Long.Open question — request-side parameters
options/OptionsChainRequest.javahas ~21Doublefilter 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
BigDecimaloperations (no mixing withdoublein one expression).BigDecimal.equalsis scale-sensitive (1.50 != 1.5); usecompareTofor value equality. Worth calling out in the migration guide, since recordequalswill inherit this.Reference
SDK Requirements §11.5 — Monetary Value Precision. Mirrors the Python decision in MarketDataApp/sdk-py#50.