Skip to content

Commit a276e15

Browse files
Merge pull request #11 from MarketDataApp/11_stocks_resource
feat: stocks resource
2 parents 67d94cc + 4a106eb commit a276e15

42 files changed

Lines changed: 4074 additions & 38 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
down the call stack.
2727

2828
### Added
29+
- **Stocks resource** (`client.stocks()`) — six endpoints, each in sync + async
30+
form: `candles`, `quote` (single symbol), `quotes` and `prices` (multi-symbol,
31+
batched into one request — one row per symbol, not a fan-out map like
32+
`options.quotes`), `news`, and `earnings`. Every endpoint takes a Builder-based
33+
per-endpoint request object. Candle resolution is a `StockResolution` value
34+
type (`DAILY`, `minutes(15)`, `hours(1)`, …) rather than an enum, since the API
35+
accepts an open-ended family of resolutions. Quote/price numeric fields are
36+
nullable (the backend nulls them for a closed/illiquid market); the OHLC and
37+
52-week columns are opt-in via `candle` / `week52`. `news` exposes the feed's
38+
scalar `updated()` off the response (distinct from each article's
39+
`publicationDate`); `earnings` tolerates the nullable fundamentals/report fields
40+
on synthesized forward-quarter rows. Mixed date/timestamp wire shapes (a daily
41+
candle's date-only `t` vs. an intraday full timestamp) decode uniformly. Carries
42+
the same universal-parameter setters, `columns` projection (with the Option A
43+
strict guarantee), and `asCsv()` facet as `options`. Intraday candle requests
44+
spanning more than ~one year are **auto-split** into year-sized sub-requests,
45+
fetched concurrently through the 50-permit pool and merged into one response
46+
(SDK requirements §12), on both the typed and CSV paths.
47+
- **Per-response rate limits** — every `MarketDataResponse` now exposes
48+
`rateLimit()` returning a `RateLimitSnapshot` parsed from that response's own
49+
`x-api-ratelimit-*` headers (request-scoped, SDK requirements §8.2), distinct
50+
from the client-level `MarketDataClient.getRateLimits()`. Applies to every
51+
resource (options/utilities/stocks) and the CSV/HTML responses.
2952
- **Options resource** (`client.options()`) — all six endpoints, each in sync +
3053
async form: `lookup`, `expirations`, `strikes`, `quote` (single contract),
31-
`quotes` (multi-contract fan-out returning
32-
`Map<String, Response<OptionsQuotes>>`), and `chain`. Every endpoint takes a
33-
Builder-based per-endpoint request object (no `String` convenience overloads).
34-
The `chain` request models its mutually-exclusive expiration and strike groups
35-
as sealed types (`ExpirationFilter`, `StrikeFilter`) so the exclusivity is
36-
compiler-enforced. Covers the `rho` greek (decoded as an optional, nullable
37-
column — absent on some feeds), the `expiration=all` filter (the full chain
38-
vs. the default front-month), and the `countback` historical-window parameter
39-
(validated: positive, and mutually exclusive with `date`/`from`).
54+
`quotes` (multi-contract fan-out returning a per-symbol
55+
`Map<String, OptionsQuotesResponse>`), and `chain`. Every endpoint takes a
56+
Builder-based per-endpoint request object (no `String` convenience overloads)
57+
and returns a named typed response (`OptionsChainResponse`,
58+
`OptionsLookupResponse`, …) implementing `MarketDataResponse<T>` — the payload
59+
is reached via `values()`. The `chain` request models its mutually-exclusive
60+
expiration and strike groups as sealed types (`ExpirationFilter`,
61+
`StrikeFilter`) so the exclusivity is compiler-enforced. Covers the `rho` greek
62+
(decoded as an optional, nullable column — absent on some feeds) plus the
63+
`Greek` enum with `presentGreeks()` / `greek(Greek)` accessors, the
64+
`expiration=all` filter (the full chain vs. the default front-month), and the
65+
`countback` historical-window parameter (validated: positive, and mutually
66+
exclusive with `date`/`from`). Universal parameters
67+
(`dateFormat`/`mode`/`limit`/`offset`) and `columns` projection are set fluently
68+
on the resource; a non-requested column decodes to `null`, while a required
69+
column you *did* request that the API omits raises a `ParseError` (Option A).
70+
The `asCsv()` facet returns CSV (`CsvResponse`) for every endpoint and adds the
71+
output-shaping `human` / `headers` params.
4072
- Project scaffold per ADRs 001–007: Gradle Kotlin DSL build, JDK 17 toolchain,
4173
`integrationTest` source set, Spotless + JaCoCo, Vanniktech Maven Publish.
4274
- `MarketDataClient` skeleton with two public constructors — a no-arg one
@@ -53,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5385
`ServerError`, `NetworkError`, `ParseError`), each carrying support context
5486
(`requestId`, `requestUrl`, `statusCode`, `timestamp`) and a
5587
`getSupportInfo()` helper.
56-
- `RateLimits` record exposed via `MarketDataClient.getRateLimits()`.
88+
- `RateLimitSnapshot` record exposed via `MarketDataClient.getRateLimits()`.
5789
- JSpecify `@NullMarked` on every public package; JSpecify on `compileOnlyApi`
5890
so consumers get the annotations at compile time without a runtime dep.
5991
- Token redaction utility (`Tokens`, package-private in the SDK root) for

CLAUDE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Repository state
66

7-
This repo contains a working Java SDK in active development on branch `clean-architecture-restart`. Gradle 9.0 (Kotlin DSL) build per ADR-003; ~48 main + ~28 test source files; full CI matrix per ADR-002. The transport, retry, rate-limit, status-cache, and exception layers are wired; only the `utilities` resource façade is implemented today — stocks/options/funds/markets resources are still to come (see "Deliberately deferred" below).
7+
This repo contains a working Java SDK in active development on branch `clean-architecture-restart`. Gradle 9.0 (Kotlin DSL) build per ADR-003; ~48 main + ~28 test source files; full CI matrix per ADR-002. The transport, retry, rate-limit, status-cache, and exception layers are wired; the `utilities`, `options`, and `stocks` resource façades are implemented today — funds/markets resources are still to come (see "Deliberately deferred" below).
88

99
Sibling repo: `../api/` is the backend (Python/Django). The Python SDK lives at `../../sdk-py/` (referenced from ADRs). The cross-language `sdk-requirements.md` is referenced as `../sdk-requirements.md` from inside `docs/`; it is canonical but not committed in this repo.
1010

@@ -83,9 +83,9 @@ The Java SDK must also satisfy the canonical, cross-language [SDK Requirements](
8383
- Coverage ratchet lives in `codecov.yml`: project status with `target: auto, threshold: 5%` (cannot drop >5 pp vs base branch) plus a patch-coverage requirement of 70 % on new code. Requires a `CODECOV_TOKEN` repo secret — without it the upload step fails because workflows pass `fail_ci_if_error: true`.
8484

8585
**Deliberately deferred (require the per-resource layer to land first):**
86-
- §1.2 resource groupings — only `client.utilities()` is wired today; `client.stocks`, `client.options`, `client.funds`, `client.markets` still to come.
87-
- §2 endpoint method coverage; §3 universal parameters; §11 wire-format decoding for resources beyond utilities. The plumbing (`ParallelArrays.zip` helper for the parallel-arrays shape, `JsonResponseParser`, `Response<T>` wrapper) is in place — each new endpoint just declares its fields and row builder.
88-
- §8 request-scoped rate-limit attachment — today the snapshot is client-level (via `client.getRateLimits()`); attaching a per-response snapshot to `Response<T>` is a small follow-up when a consumer needs it.
86+
- §1.2 resource groupings — `client.utilities()`, `client.options()`, and `client.stocks()` are wired today; `client.funds`, `client.markets` still to come.
87+
- §2 endpoint method coverage; §3 universal parameters; §11 wire-format decoding for the remaining resources (funds, markets). The plumbing (`ParallelArrays.zip` helper for the parallel-arrays shape, `JsonResponseParser`, the `MarketDataResponse<T>` named-response types) is in place — each new endpoint just declares its fields and row builder.
88+
- ~~§8 request-scoped rate-limit attachment~~ **DONE** — every `MarketDataResponse` now exposes `rateLimit()`, parsed from that response's own `x-api-ratelimit-*` headers (request-scoped), alongside the client-level `client.getRateLimits()`.
8989
- §13 100% coverage threshold via JaCoCo `violationRules`; deferred until the resource layer lands so the threshold meaningfully ratchets functional code, not just scaffolding.
9090

9191
When picking up new work, check this list before reaching for the SDK requirements doc — most foundational rules are already encoded in code; missing pieces are deferred deliberately, not by accident.

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ demo-concurrency: ## 50-permit semaphore (needs mock-server)
9191
demo-options: ## Full options surface: every endpoint + all params, CSV facet, columns, Option A (needs mock-server)
9292
cd $(CONSUMER_DIR) && ./gradlew runOptions
9393

94+
.PHONY: demo-stocks
95+
demo-stocks: ## Full stocks surface: every endpoint + all params, CSV facet, columns, Option A (needs mock-server)
96+
cd $(CONSUMER_DIR) && ./gradlew runStocks
97+
9498
.PHONY: demos-all
9599
demos-all: ## Run every mock-server-based demo back-to-back (needs mock-server)
96-
cd $(CONSUMER_DIR) && ./gradlew runDemoConfig runExceptions runRetry runResponse runConcurrency runOptions
100+
cd $(CONSUMER_DIR) && ./gradlew runDemoConfig runExceptions runRetry runResponse runConcurrency runOptions runStocks

README.md

Lines changed: 100 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Market Data Java SDK
22

33
Java SDK for the [Market Data API](https://www.marketdata.app/). **Pre-release**
4-
— the `utilities` and `options` resources are implemented; `stocks`, `funds`,
4+
— the `utilities`, `options`, and `stocks` resources are implemented; `funds`
55
and `markets` are forthcoming. The build, package layout, configuration cascade,
66
exception taxonomy, and Kotlin-interop foundations are in place.
77

@@ -48,8 +48,10 @@ MarketDataClient().use { client ->
4848

4949
Every response implements `MarketDataResponse<T>`: `values()` returns the typed payload
5050
(typed per endpoint — a `List`, a scalar `String`, …), and the same metadata accessors
51-
(`statusCode()`, `isNoData()`, `requestId()`, `json()`, `saveToFile(path)`) are available on
52-
every response, on every resource.
51+
(`statusCode()`, `isNoData()`, `requestId()`, `rateLimit()`, `json()`, `saveToFile(path)`) are
52+
available on every response, on every resource. `rateLimit()` returns the rate-limit snapshot
53+
parsed from *that* response's headers (request-scoped), distinct from the client-level
54+
`client.getRateLimits()`.
5355

5456
## Options
5557

@@ -149,6 +151,92 @@ With `columns`, a field you didn't request decodes to `null` (no error); a **req
149151
you *did* request (or didn't project away) that the API omits raises a `ParseError` — so a
150152
`null` never silently hides a dropped field.
151153

154+
## Stocks
155+
156+
Reached via `client.stocks()`. Same conventions as `options`: every endpoint has a
157+
synchronous method and an `…Async` variant, takes a Builder-based request object, and returns
158+
a typed `MarketDataResponse` (payload via `values()`). The universal-parameter setters
159+
(`dateFormat`/`mode`/`limit`/`offset`/`columns`) and the `asCsv()` facet work identically.
160+
161+
| Method | Purpose |
162+
|--------|---------|
163+
| `candles` | Historical OHLCV candles for a symbol at a given `StockResolution` |
164+
| `quote` | Real-time quote for a single symbol |
165+
| `quotes` | Quotes for many symbols — batched in **one** request, one row per symbol |
166+
| `prices` | Lightweight price snapshot (mid/change) for many symbols |
167+
| `news` | Recent news articles for a symbol |
168+
| `earnings` | EPS history and the forward earnings calendar |
169+
170+
> Unlike `options.quotes` (which fans out one request per contract and returns a per-symbol
171+
> map), the stocks backend accepts a comma list in a single request — so `stocks.quotes` and
172+
> `stocks.prices` return a single response with one row per symbol.
173+
174+
### Candles
175+
176+
`StockResolution` is a value type, not an enum — the API accepts an open-ended family of
177+
resolutions, so use the factories (`DAILY`, `minutes(15)`, `hours(1)`, `days(2)`, …). An
178+
**intraday** request spanning more than ~one year is automatically split into year-sized
179+
sub-requests, fetched concurrently and merged into one response — transparent to the caller:
180+
181+
#### Java
182+
183+
```java
184+
try (var client = new MarketDataClient()) {
185+
var resp = client.stocks().candles(
186+
StockCandlesRequest.builder(StockResolution.DAILY, "AAPL")
187+
.from(LocalDate.now().minusMonths(1))
188+
.to(LocalDate.now())
189+
.build());
190+
191+
for (StockCandle c : resp.values()) { // values() is a List<StockCandle>
192+
System.out.printf("%s O=%s H=%s L=%s C=%s V=%s%n",
193+
c.time(), c.open(), c.high(), c.low(), c.close(), c.volume());
194+
}
195+
}
196+
```
197+
198+
#### Kotlin
199+
200+
```kotlin
201+
MarketDataClient().use { client ->
202+
val resp = client.stocks().candles(
203+
StockCandlesRequest.builder(StockResolution.DAILY, "AAPL")
204+
.from(LocalDate.now().minusMonths(1))
205+
.to(LocalDate.now())
206+
.build()
207+
)
208+
resp.values().forEach { c ->
209+
println("${c.time} O=${c.open} H=${c.high} L=${c.low} C=${c.close} V=${c.volume}")
210+
}
211+
}
212+
```
213+
214+
### Quotes, prices, news, earnings
215+
216+
```java
217+
// Multi-symbol quote — one batched request, one row per symbol:
218+
StockQuotesResponse q = client.stocks().quotes(
219+
StockQuotesRequest.builder("AAPL", "MSFT")
220+
.candle(true) // opt-in OHLC columns
221+
.week52(true) // opt-in 52-week high/low columns
222+
.build());
223+
224+
// News — articles plus the feed's latest-update time as a scalar off the response:
225+
StockNewsResponse news = client.stocks().news(StockNewsRequest.of("AAPL"));
226+
news.values().forEach(a -> System.out.println(a.publicationDate() + " " + a.headline()));
227+
System.out.println("feed updated: " + news.updated());
228+
229+
// Earnings — fundamentals and report fields are nullable on forward-quarter rows:
230+
client.stocks().earnings(StockEarningsRequest.of("AAPL"))
231+
.values()
232+
.forEach(e -> System.out.println("FY" + e.fiscalYear() + " Q" + e.fiscalQuarter()
233+
+ " reportedEPS=" + e.reportedEPS()));
234+
```
235+
236+
Stock quote/price numeric fields are `@Nullable` (the backend nulls them for a closed or
237+
illiquid market), and the OHLC / 52-week columns appear only when opted in via `candle` /
238+
`week52`.
239+
152240
## Configuration
153241

154242
Values are resolved through this cascade (highest priority first):
@@ -177,7 +265,7 @@ Values are resolved through this cascade (highest priority first):
177265

178266
The corresponding per-call setters — `dateFormat`/`limit`/`offset`/`mode`/`columns` on the
179267
resource, plus `human`/`headers` and `asCsv()` on the CSV facet — are exposed on `options`
180-
today (and on every resource as it lands). Auto-applying these env-var values as request
268+
and `stocks` today (and on every resource as it lands). Auto-applying these env-var values as request
181269
*defaults* (`DATE_FORMAT`, `COLUMNS`, `ADD_HEADERS`, `USE_HUMAN_READABLE`, `MODE`,
182270
`OUTPUT_FORMAT`) is still reserved.
183271

@@ -244,14 +332,18 @@ isn't an exact match is rejected before any request is made.
244332

245333
```
246334
com.marketdata.sdk # MarketDataClient, RateLimits, the resource façades
247-
# (UtilitiesResource, OptionsResource, OptionsCsvResource),
248-
# and MarketDataResponse<T> + the named response types
249-
# (OptionsChainResponse, CsvResponse, …) — public;
250-
# Configuration, EnvVars, Tokens, Version are
335+
# (UtilitiesResource, OptionsResource, StocksResource,
336+
# OptionsCsvResource, StocksCsvResource), and
337+
# MarketDataResponse<T> + the named response types
338+
# (OptionsChainResponse, StockCandlesResponse, CsvResponse, …)
339+
# — public; Configuration, EnvVars, Tokens, Version are
251340
# package-private and not part of the API
252341
com.marketdata.sdk.options # Options request builders + row records
253342
# (OptionsChainRequest, OptionQuote, sealed
254343
# ExpirationFilter / StrikeFilter, Greek, …)
344+
com.marketdata.sdk.stocks # Stocks request builders + row records
345+
# (StockCandlesRequest, StockCandle, StockQuote,
346+
# StockEarning, StockResolution, …)
255347
com.marketdata.sdk.exception # Sealed MarketDataException hierarchy + ErrorContext
256348
```
257349

0 commit comments

Comments
 (0)