Skip to content

feat: stocks resource#11

Merged
MarketDataDev03 merged 4 commits into
mainfrom
11_stocks_resource
Jun 16, 2026
Merged

feat: stocks resource#11
MarketDataDev03 merged 4 commits into
mainfrom
11_stocks_resource

Conversation

@MarketDataDev03

@MarketDataDev03 MarketDataDev03 commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

feat: stocks resource

Branch 11_stocks_resourcemain

Adds the stocks resource (client.stocks()) on top of the response-model + parameter conventions established by options. Reuses the transport/retry/rate-limit/response foundation, with two additive shared-layer changes (a tolerant date parser, and a per-response rate-limit accessor on MarketDataResponse). Reviewer's companion: docs/STOCKS_REVIEW_GUIDE.md.

What's included

  • Six endpoints, each in sync + …Async form, every one taking a Builder-based per-endpoint request and returning a named MarketDataResponse<T> (payload via values()):
    • candles — OHLCV series for a StockResolution.
    • quote — single-symbol quote (/stocks/quotes/{symbol}/).
    • quotes / prices — multi-symbol, batched into one request (comma list), one row per symbol. Unlike options.quotes (per-contract fan-out → map), these return a single response.
    • news — articles + the feed's scalar updated() off the response.
    • earnings — EPS history + the forward calendar.
  • StockResolution — a value type (not an enum): DAILY/WEEKLY/MONTHLY/YEARLY constants + minutes(int)/hours(int)/days(int)/weeks(int)/months(int)/years(int) factories + of(String), since the API accepts an open-ended family of resolutions.
  • Nullable fields + columns + Option A — every row field is @Nullable boxed (so columns can project, and so the backend's NaN→null over closed/illiquid markets decodes cleanly); a requested-but-omitted required column still raises ParseError. Quote OHLC and 52-week columns are opt-in via candle / week52.
  • CSV facet (asCsv()CsvResponse, adds human/headers) for every endpoint; HTML facet built but package-private (backend serves no HTML).
  • Wire-format: candles/quotes/prices/earnings decode via the generic parallel-arrays helper; news has a hand-written StockNewsDeserializer (per-row arrays + a scalar updated at the root). New MarketDataDates.parseDateOrTimestampField tolerates a daily candle's date-only t and an intraday full timestamp uniformly.
  • Candle auto-chunking (§12) — an intraday request with a from bound spanning > ~1 year is split into year-sized sub-requests (StockResolution.isIntraday() + candleChunks), fetched concurrently through the 50-permit pool and merged into one response (typed and CSV paths). Mirrors the Python SDK's split_dates_by_timeframe.
  • Per-response rate limits (§8.2)MarketDataResponse.rateLimit() returns a RateLimitSnapshot parsed from that response's own x-api-ratelimit-* headers (request-scoped, distinct from client-level getRateLimits()). SDK-wide: also covers options/utilities and the CSV/HTML responses.
  • Examples: StocksApp (every endpoint + CSV facet + columns + Option A + chunking + per-response rate-limit, against the mock server; make demo-stocks); the QuickstartApp stocks section enabled.
  • Docs: README ## Stocks section (Java + Kotlin), CHANGELOG, CLAUDE.md state.

Shared layers (additive changes)

  • MarketDataClient wires client.stocks().
  • MarketDataDates gains parseDateOrTimestampField (existing parsers untouched).
  • MarketDataResponse gains rateLimit(); AbstractMarketDataResponse parses it once from the envelope headers, so every existing response type (options/utilities/CSV/HTML) gets it for free — the only implementor of the interface.
  • Everything else — transport, retry, rate-limit parsing (RateLimitHeaders), status-cache, exceptions, ParallelArrays, JsonResponseParser, RequestConfig, the response model — is reused as-is from the options PR.

@MarketDataDev03 MarketDataDev03 changed the title 11 stocks resource feat: stocks resource Jun 9, 2026
@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

The author of this PR, MarketDataDev03, is not an activated member of this organization on Codecov.
Please activate this user on Codecov to display this PR comment.
Coverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.
Please don't hesitate to email us at support@codecov.io with any questions.

@MarketDataDev03 MarketDataDev03 marked this pull request as ready for review June 10, 2026 12:41
@MarketDataDev03 MarketDataDev03 self-assigned this Jun 10, 2026
@MarketDataDev01

Copy link
Copy Markdown
Collaborator

Review finding: news silently breaks under a columns projection

columns(...) is offered uniformly on StocksResource, but news doesn't honor it the way the other four endpoints do. StockNewsDeserializer zips all five article columns as required (ParallelArrays.zip(p, root, ARTICLE_FIELDS, …)), so as soon as the API legitimately omits a column — which is exactly what a columns projection asks it to do — the zip aborts.

Repro (reduced from a StocksResourceTest-style mock; backend honors columns=headline and returns only that array):

// mock body: {"s":"ok","headline":["Record Q4","New product"]}
client.stocks().columns("headline").news(StockNewsRequest.of("AAPL"));

Result:

com.marketdata.sdk.exception.ParseError
  Caused by: JsonMappingException: missing or non-array field: symbol

The contrast is the problem — same setter, same user mental model, different outcome:

  • client.stocks().columns("mid").quote(...) → works, nulls the unrequested fields (Option A path)
  • client.stocks().columns("headline").news(...)ParseError

The generic endpoints route through rowsDeserializer(...) with an empty required-list + validateRequestedColumns (Option A), so projected-away columns decode to null. news reads every article field strictly, so it can't. No existing test exercises the news + columns path, which is why it slipped through.

Severity: medium — not an exotic edge; it's the documented columns feature applied to one of the six endpoints.

Suggested fix: give StockNewsDeserializer the same all-optional + Option A treatment as the generic path (article fields optional at the wire level, validated against the requested column set), keeping only the scalar-updated handling hand-written. Plus a regression test for news + columns.

(Alternatively, if columns on news is intentionally unsupported, document that and fail with a clearer message.)

@MarketDataDev03

Copy link
Copy Markdown
Collaborator Author

Review finding: news silently breaks under a columns projection

columns(...) is offered uniformly on StocksResource, but news doesn't honor it the way the other four endpoints do. StockNewsDeserializer zips all five article columns as required (ParallelArrays.zip(p, root, ARTICLE_FIELDS, …)), so as soon as the API legitimately omits a column — which is exactly what a columns projection asks it to do — the zip aborts.

Repro (reduced from a StocksResourceTest-style mock; backend honors columns=headline and returns only that array):

// mock body: {"s":"ok","headline":["Record Q4","New product"]}
client.stocks().columns("headline").news(StockNewsRequest.of("AAPL"));

Result:

com.marketdata.sdk.exception.ParseError
  Caused by: JsonMappingException: missing or non-array field: symbol

The contrast is the problem — same setter, same user mental model, different outcome:

  • client.stocks().columns("mid").quote(...) → works, nulls the unrequested fields (Option A path)
  • client.stocks().columns("headline").news(...)ParseError

The generic endpoints route through rowsDeserializer(...) with an empty required-list + validateRequestedColumns (Option A), so projected-away columns decode to null. news reads every article field strictly, so it can't. No existing test exercises the news + columns path, which is why it slipped through.

Severity: medium — not an exotic edge; it's the documented columns feature applied to one of the six endpoints.

Suggested fix: give StockNewsDeserializer the same all-optional + Option A treatment as the generic path (article fields optional at the wire level, validated against the requested column set), keeping only the scalar-updated handling hand-written. Plus a regression test for news + columns.

(Alternatively, if columns on news is intentionally unsupported, document that and fail with a clearer message.)

Good catch. I didn't want to support it because StockNewsArticle is non-null by contract, and honoring a
projection would force me to make every field nullable for all callers — taxing the common path with
null-checks just to enable a niche projection on a 5-field endpoint.

So client.stocks().columns(...).news(...) now fails fast with a clear IllegalArgumentException pointing at
the CSV facet (asCsv().columns(...).news(...)), which still works since CSV is raw text. That also replaces
the opaque ParseError you hit. I surfaced it as a failed future (not a sync throw) so async callers catch it
normally, and added regression tests. If typed projection on news is ever needed, Option A is a clean
major-version change.

@MarketDataDev03 MarketDataDev03 merged commit a276e15 into main Jun 16, 2026
5 checks passed
@MarketDataDev03 MarketDataDev03 deleted the 11_stocks_resource branch June 16, 2026 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants