Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 53 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
down the call stack.

### Added
- **Funds resource** (`client.funds()`) — the single funds endpoint, `candles`,
in sync + async form, taking a Builder-based `FundCandlesRequest` (window:
`date` xor `from`/`to`/`countback`, plus `exchange`/`country`/`adjustsplits`/
`adjustdividends`). Fund candles are NAV series: OHLC only (no volume column),
daily-and-up resolutions only — `FundResolution` models `DAILY`/`WEEKLY`/
`MONTHLY`/`YEARLY` and `days/weeks/months/years(n)`, with no intraday
factories (the API rejects intraday tokens for funds) and therefore no §12
auto-chunking. Universal params (`dateFormat`/`mode`/`limit`/`offset`/
`columns`) as configured copies, CSV facet via `asCsv()` with the
`human`/`headers` shaping params, and the same nullable-fields + `columns` +
Option A decoding contract as stocks/options.
- **Stocks resource** (`client.stocks()`) — six endpoints, each in sync + async
form: `candles`, `quote` (single symbol), `quotes` and `prices` (multi-symbol,
batched into one request — one row per symbol, not a fan-out map like
`options.quotes`), `news`, and `earnings`. Every endpoint takes a Builder-based
per-endpoint request object. Candle resolution is a `StockResolution` value
type (`DAILY`, `minutes(15)`, `hours(1)`, …) rather than an enum, since the API
accepts an open-ended family of resolutions. Quote/price numeric fields are
nullable (the backend nulls them for a closed/illiquid market); the OHLC and
52-week columns are opt-in via `candle` / `week52`. `news` exposes the feed's
scalar `updated()` off the response (distinct from each article's
`publicationDate`); `earnings` tolerates the nullable fundamentals/report fields
on synthesized forward-quarter rows. Mixed date/timestamp wire shapes (a daily
candle's date-only `t` vs. an intraday full timestamp) decode uniformly. Carries
the same universal-parameter setters, `columns` projection (with the Option A
strict guarantee), and `asCsv()` facet as `options`. Intraday candle requests
spanning more than ~one year are **auto-split** into year-sized sub-requests,
fetched concurrently through the 50-permit pool and merged into one response
(SDK requirements §12), on both the typed and CSV paths.
- **Per-response rate limits** — every `MarketDataResponse` now exposes
`rateLimit()` returning a `RateLimitSnapshot` parsed from that response's own
`x-api-ratelimit-*` headers (request-scoped, SDK requirements §8.2), distinct
from the client-level `MarketDataClient.getRateLimits()`. Applies to every
resource (options/utilities/stocks) and the CSV/HTML responses.
- **Options resource** (`client.options()`) — all six endpoints, each in sync +
async form: `lookup`, `expirations`, `strikes`, `quote` (single contract),
`quotes` (multi-contract fan-out returning
`Map<String, Response<OptionsQuotes>>`), and `chain`. Every endpoint takes a
Builder-based per-endpoint request object (no `String` convenience overloads).
The `chain` request models its mutually-exclusive expiration and strike groups
as sealed types (`ExpirationFilter`, `StrikeFilter`) so the exclusivity is
compiler-enforced. Covers the `rho` greek (decoded as an optional, nullable
column — absent on some feeds), the `expiration=all` filter (the full chain
vs. the default front-month), and the `countback` historical-window parameter
(validated: positive, and mutually exclusive with `date`/`from`).
`quotes` (multi-contract fan-out returning a per-symbol
`Map<String, OptionsQuotesResponse>`), and `chain`. Every endpoint takes a
Builder-based per-endpoint request object (no `String` convenience overloads)
and returns a named typed response (`OptionsChainResponse`,
`OptionsLookupResponse`, …) implementing `MarketDataResponse<T>` — the payload
is reached via `values()`. The `chain` request models its mutually-exclusive
expiration and strike groups as sealed types (`ExpirationFilter`,
`StrikeFilter`) so the exclusivity is compiler-enforced. Covers the `rho` greek
(decoded as an optional, nullable column — absent on some feeds) plus the
`Greek` enum with `presentGreeks()` / `greek(Greek)` accessors, the
`expiration=all` filter (the full chain vs. the default front-month), and the
`countback` historical-window parameter (validated: positive, and mutually
exclusive with `date`/`from`). Universal parameters
(`dateFormat`/`mode`/`limit`/`offset`) and `columns` projection are set fluently
on the resource; a non-requested column decodes to `null`, while a required
column you *did* request that the API omits raises a `ParseError` (Option A).
The `asCsv()` facet returns CSV (`CsvResponse`) for every endpoint and adds the
output-shaping `human` / `headers` params.
- Project scaffold per ADRs 001–007: Gradle Kotlin DSL build, JDK 17 toolchain,
`integrationTest` source set, Spotless + JaCoCo, Vanniktech Maven Publish.
- `MarketDataClient` skeleton with two public constructors — a no-arg one
Expand All @@ -53,7 +96,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`ServerError`, `NetworkError`, `ParseError`), each carrying support context
(`requestId`, `requestUrl`, `statusCode`, `timestamp`) and a
`getSupportInfo()` helper.
- `RateLimits` record exposed via `MarketDataClient.getRateLimits()`.
- `RateLimitSnapshot` record exposed via `MarketDataClient.getRateLimits()`.
- JSpecify `@NullMarked` on every public package; JSpecify on `compileOnlyApi`
so consumers get the annotations at compile time without a runtime dep.
- Token redaction utility (`Tokens`, package-private in the SDK root) for
Expand Down
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Repository state

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).
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`, `stocks`, and `funds` resource façades are implemented today — the markets resource is still to come (see "Deliberately deferred" below).

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.

Expand Down Expand Up @@ -83,9 +83,9 @@ The Java SDK must also satisfy the canonical, cross-language [SDK Requirements](
- 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`.

**Deliberately deferred (require the per-resource layer to land first):**
- §1.2 resource groupings — only `client.utilities()` is wired today; `client.stocks`, `client.options`, `client.funds`, `client.markets` still to come.
- §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.
- §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.
- §1.2 resource groupings — `client.utilities()`, `client.options()`, `client.stocks()`, and `client.funds()` are wired today; `client.markets` still to come.
- §2 endpoint method coverage; §3 universal parameters; §11 wire-format decoding for the remaining resource (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.
- ~~§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()`.
- §13 100% coverage threshold via JaCoCo `violationRules`; deferred until the resource layer lands so the threshold meaningfully ratchets functional code, not just scaffolding.

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.
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ demo-concurrency: ## 50-permit semaphore (needs mock-server)
demo-options: ## Full options surface: every endpoint + all params, CSV facet, columns, Option A (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runOptions

.PHONY: demo-stocks
demo-stocks: ## Full stocks surface: every endpoint + all params, CSV facet, columns, Option A (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runStocks

.PHONY: demo-funds
demo-funds: ## Full funds surface: candles + all params (no volume/intraday/chunking), CSV facet, columns, Option A (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runFunds

.PHONY: demos-all
demos-all: ## Run every mock-server-based demo back-to-back (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runDemoConfig runExceptions runRetry runResponse runConcurrency runOptions
cd $(CONSUMER_DIR) && ./gradlew runDemoConfig runExceptions runRetry runResponse runConcurrency runOptions runStocks runFunds
108 changes: 100 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Market Data Java SDK

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

Expand Down Expand Up @@ -48,8 +48,10 @@ MarketDataClient().use { client ->

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

## Options

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

## Stocks

Reached via `client.stocks()`. Same conventions as `options`: every endpoint has a
synchronous method and an `…Async` variant, takes a Builder-based request object, and returns
a typed `MarketDataResponse` (payload via `values()`). The universal-parameter setters
(`dateFormat`/`mode`/`limit`/`offset`/`columns`) and the `asCsv()` facet work identically.

| Method | Purpose |
|--------|---------|
| `candles` | Historical OHLCV candles for a symbol at a given `StockResolution` |
| `quote` | Real-time quote for a single symbol |
| `quotes` | Quotes for many symbols — batched in **one** request, one row per symbol |
| `prices` | Lightweight price snapshot (mid/change) for many symbols |
| `news` | Recent news articles for a symbol |
| `earnings` | EPS history and the forward earnings calendar |

> Unlike `options.quotes` (which fans out one request per contract and returns a per-symbol
> map), the stocks backend accepts a comma list in a single request — so `stocks.quotes` and
> `stocks.prices` return a single response with one row per symbol.

### Candles

`StockResolution` is a value type, not an enum — the API accepts an open-ended family of
resolutions, so use the factories (`DAILY`, `minutes(15)`, `hours(1)`, `days(2)`, …). An
**intraday** request spanning more than ~one year is automatically split into year-sized
sub-requests, fetched concurrently and merged into one response — transparent to the caller:

#### Java

```java
try (var client = new MarketDataClient()) {
var resp = client.stocks().candles(
StockCandlesRequest.builder(StockResolution.DAILY, "AAPL")
.from(LocalDate.now().minusMonths(1))
.to(LocalDate.now())
.build());

for (StockCandle c : resp.values()) { // values() is a List<StockCandle>
System.out.printf("%s O=%s H=%s L=%s C=%s V=%s%n",
c.time(), c.open(), c.high(), c.low(), c.close(), c.volume());
}
}
```

#### Kotlin

```kotlin
MarketDataClient().use { client ->
val resp = client.stocks().candles(
StockCandlesRequest.builder(StockResolution.DAILY, "AAPL")
.from(LocalDate.now().minusMonths(1))
.to(LocalDate.now())
.build()
)
resp.values().forEach { c ->
println("${c.time} O=${c.open} H=${c.high} L=${c.low} C=${c.close} V=${c.volume}")
}
}
```

### Quotes, prices, news, earnings

```java
// Multi-symbol quote — one batched request, one row per symbol:
StockQuotesResponse q = client.stocks().quotes(
StockQuotesRequest.builder("AAPL", "MSFT")
.candle(true) // opt-in OHLC columns
.week52(true) // opt-in 52-week high/low columns
.build());

// News — articles plus the feed's latest-update time as a scalar off the response:
StockNewsResponse news = client.stocks().news(StockNewsRequest.of("AAPL"));
news.values().forEach(a -> System.out.println(a.publicationDate() + " " + a.headline()));
System.out.println("feed updated: " + news.updated());

// Earnings — fundamentals and report fields are nullable on forward-quarter rows:
client.stocks().earnings(StockEarningsRequest.of("AAPL"))
.values()
.forEach(e -> System.out.println("FY" + e.fiscalYear() + " Q" + e.fiscalQuarter()
+ " reportedEPS=" + e.reportedEPS()));
```

Stock quote/price numeric fields are `@Nullable` (the backend nulls them for a closed or
illiquid market), and the OHLC / 52-week columns appear only when opted in via `candle` /
`week52`.

## Configuration

Values are resolved through this cascade (highest priority first):
Expand Down Expand Up @@ -177,7 +265,7 @@ Values are resolved through this cascade (highest priority first):

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

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

```
com.marketdata.sdk # MarketDataClient, RateLimits, the resource façades
# (UtilitiesResource, OptionsResource, OptionsCsvResource),
# and MarketDataResponse<T> + the named response types
# (OptionsChainResponse, CsvResponse, …) — public;
# Configuration, EnvVars, Tokens, Version are
# (UtilitiesResource, OptionsResource, StocksResource,
# OptionsCsvResource, StocksCsvResource), and
# MarketDataResponse<T> + the named response types
# (OptionsChainResponse, StockCandlesResponse, CsvResponse, …)
# — public; Configuration, EnvVars, Tokens, Version are
# package-private and not part of the API
com.marketdata.sdk.options # Options request builders + row records
# (OptionsChainRequest, OptionQuote, sealed
# ExpirationFilter / StrikeFilter, Greek, …)
com.marketdata.sdk.stocks # Stocks request builders + row records
# (StockCandlesRequest, StockCandle, StockQuote,
# StockEarning, StockResolution, …)
com.marketdata.sdk.exception # Sealed MarketDataException hierarchy + ErrorContext
```

Expand Down
Loading
Loading