Skip to content
Merged
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
down the call stack.

### Added
- **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`).
- 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 Down
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,17 @@ demo-retry: ## Retry, Retry-After, preflight (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runRetry

.PHONY: demo-response
demo-response: ## Response<T> surface features (needs mock-server)
demo-response: ## MarketDataResponse surface features (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runResponse

.PHONY: demo-concurrency
demo-concurrency: ## 50-permit semaphore (needs mock-server)
cd $(CONSUMER_DIR) && ./gradlew runConcurrency

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

.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
cd $(CONSUMER_DIR) && ./gradlew runDemoConfig runExceptions runRetry runResponse runConcurrency runOptions
137 changes: 125 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Market Data Java SDK

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

## Requirements

Expand Down Expand Up @@ -32,18 +32,123 @@ common path is two lines:

```java
try (var client = new MarketDataClient()) {
// endpoint methods land in subsequent iterations
var resp = client.options().expirations(OptionsExpirationsRequest.of("AAPL"));
System.out.println(resp.values()); // values() is the typed payload (a List<ZonedDateTime>)
}
```

### Kotlin

```kotlin
MarketDataClient().use { client ->
// endpoint methods land in subsequent iterations
val resp = client.options().expirations(OptionsExpirationsRequest.of("AAPL"))
println(resp.values()) // List<ZonedDateTime>
}
```

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.

## Options

Reached via `client.options()`. Every endpoint has a synchronous method and an
`…Async` variant returning `CompletableFuture`, and takes a Builder-based
request object — there are no `String` convenience overloads, so the call shape
is uniform across the SDK regardless of how many parameters an endpoint has.

| Method | Purpose |
|--------|---------|
| `lookup` | Resolve a human description (`"AAPL 1/16/2026 $200 Call"`) to an OCC symbol |
| `expirations` | Expiration dates for an underlying |
| `strikes` | Strike ladder per expiration |
| `quote` | Quote for a single OCC option symbol |
| `quotes` | Quotes for many symbols — fans out concurrently, returns a per-symbol map |
| `chain` | Full option chain with the rich filter surface |

### Chain with filters

The `chain` request exposes the API's full filter set. Mutually-exclusive groups
(expiration, strike) are modeled as sealed types, so the compiler lets you pick
only one variant per group:

#### Java

```java
try (var client = new MarketDataClient()) {
var resp = client.options().chain(
OptionsChainRequest.builder("AAPL")
.expirationFilter(ExpirationFilter.all()) // every expiration, not just front-month
.strikeFilter(StrikeFilter.range(150, 200)) // 150 <= strike <= 200
.side(OptionSide.CALL)
.strikeLimit(5)
.build());

for (OptionQuote q : resp.values()) { // values() is a List<OptionQuote>
System.out.printf("%s delta=%s rho=%s%n",
q.optionSymbol(), q.delta(), q.rho()); // delta/rho are @Nullable Double
}
}
```

#### Kotlin

```kotlin
MarketDataClient().use { client ->
val resp = client.options().chain(
OptionsChainRequest.builder("AAPL")
.expirationFilter(ExpirationFilter.all())
.strikeFilter(StrikeFilter.range(150.0, 200.0))
.side(OptionSide.CALL)
.strikeLimit(5)
.build()
)
resp.values().forEach { q ->
println("${q.optionSymbol} delta=${q.delta} rho=${q.rho}") // delta/rho are nullable
}
}
```

### Multiple quotes

`quotes` fans out one request per symbol concurrently and returns a
`Map<String, OptionsQuotesResponse>` keyed by the input symbol, so per-symbol
status and errors stay observable. `countback` caps the historical series to the
N most recent rows before `to`:

```java
Map<String, OptionsQuotesResponse> bySymbol = client.options().quotes(
OptionsQuotesRequest.builder("AAPL250117C00150000", "AAPL250117P00150000")
.to(LocalDate.now())
.countback(5) // at most 5 EOD rows per symbol, before `to`
.build());

bySymbol.forEach((sym, resp) -> System.out.println(sym + " → " + resp.values().size() + " rows"));
```

### Universal parameters & CSV

Universal parameters are set fluently on the resource (an immutable configured value, so you
can "configure once, call many"); `columns` projects the response to a subset of fields, and
`asCsv()` selects a CSV view of any endpoint:

```java
// type-preserving universal params + column projection (typed):
var chain = client.options()
.dateFormat(DateFormat.TIMESTAMP).mode(Mode.DELAYED).limit(50)
.columns("optionSymbol", "strike", "delta") // fields you don't request come back null
.chain(OptionsChainRequest.of("AAPL"));

// CSV facet (adds human/headers, which only make sense for CSV):
CsvResponse csv = client.options().asCsv().columns("optionSymbol", "strike").chain(req);
csv.saveToFile(Path.of("aapl-chain.csv"));
```

With `columns`, a field you didn't request decodes to `null` (no error); a **required** field
you *did* request (or didn't project away) that the API omits raises a `ParseError` — so a
`null` never silently hides a dropped field.

## Configuration

Values are resolved through this cascade (highest priority first):
Expand All @@ -70,9 +175,11 @@ Values are resolved through this cascade (highest priority first):
| `MARKETDATA_USE_HUMAN_READABLE` | Human-readable field names | `false` |
| `MARKETDATA_MODE` | Data mode (live/cached/delayed) | `live` |

Endpoint-shape variables (`OUTPUT_FORMAT`, `DATE_FORMAT`, `COLUMNS`,
`ADD_HEADERS`, `USE_HUMAN_READABLE`, `MODE`) are reserved here and will be
honored when the request layer lands.
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
*defaults* (`DATE_FORMAT`, `COLUMNS`, `ADD_HEADERS`, `USE_HUMAN_READABLE`, `MODE`,
`OUTPUT_FORMAT`) is still reserved.

### Demo mode

Expand Down Expand Up @@ -136,9 +243,15 @@ isn't an exact match is rejected before any request is made.
## Package layout

```
com.marketdata.sdk # MarketDataClient + RateLimits (public);
# Configuration, EnvVars, Tokens, Version
# are package-private and not part of the API
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
# 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.exception # Sealed MarketDataException hierarchy + ErrorContext
```

Expand Down
Loading
Loading