This is the official JVM SDK for Market Data, built for Java and Kotlin alike. It provides developers with a powerful, easy-to-use interface to obtain real-time and historical financial data. Ideal for building financial applications, trading bots, and investment strategies.
- First-Class Kotlin Support: Every public type is
@NullMarked(JSpecify), so Kotlin sees real nullable / non-null types instead of platform types — no Kotlin artifact, no coroutines dependency, idiomatic from both languages - Sync & Async Parity: Every endpoint offers a blocking method and a
CompletableFuturevariant, bridgeable to Kotlin coroutines viaawait() - Real-time Stock Data: Prices, quotes, candles (OHLCV), earnings, and news
- Options Trading Data: Complete options chains, expirations, strikes, quotes, and lookup
- Mutual Funds: Historical candles and pricing data
- Market Status: Real-time market open/closed status for multiple countries
- Multiple Output Formats: Typed objects, JSON, CSV, or HTML
- Built-in Retry Logic: Automatic retry with exponential backoff for reliable data fetching
- Rate Limit Tracking: Per-response and client-level rate-limit snapshots
- Type-Safe: Records, a sealed exception hierarchy, and builder-based request objects
- Zero Config: Works out of the box with sensible defaults
- JDK 17 or newer. The published artifact is compiled with
javac --release 17. Tests run on JDK 17, 21, and 25. - Jackson 2.18+ on the runtime classpath. Pulled transitively; consumers may align to a newer 2.x.
// build.gradle.kts
dependencies {
implementation("app.marketdata:marketdata-sdk-java:1.0.0")
}The SDK reads MARKETDATA_TOKEN from the environment by default, so the
common path is two lines:
try (var client = new MarketDataClient()) {
var resp = client.options().expirations(OptionsExpirationsRequest.of("AAPL"));
System.out.println(resp.values()); // values() is the typed payload (a List<ZonedDateTime>)
}MarketDataClient().use { client ->
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(), 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().
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 |
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 |
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:
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
}
}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
}
}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:
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 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:
// 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.
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 — sostocks.quotesandstocks.pricesreturn a single response with one row per symbol.
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:
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());
}
}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}")
}
}// 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.
Values are resolved through this cascade (highest priority first):
- Explicit constructor parameters —
apiKey,baseUrl,apiVersion(passed tonew MarketDataClient(apiKey, baseUrl, apiVersion, validateOnStartup); the no-argnew MarketDataClient()skips this step and starts at #2) - Environment variables (table below)
.envfile in the current working directory- Built-in defaults
| Variable | Purpose | Default |
|---|---|---|
MARKETDATA_TOKEN |
API authentication token | (none — demo mode) |
MARKETDATA_BASE_URL |
API base URL | https://api.marketdata.app |
MARKETDATA_API_VERSION |
API version | v1 |
MARKETDATA_LOGGING_LEVEL |
SDK logging level | INFO |
MARKETDATA_OUTPUT_FORMAT |
Default output format | (language default) |
MARKETDATA_DATE_FORMAT |
Default date format | timestamp |
MARKETDATA_COLUMNS |
Columns to include | (all) |
MARKETDATA_ADD_HEADERS |
Include headers in CSV | true |
MARKETDATA_USE_HUMAN_READABLE |
Human-readable field names | false |
MARKETDATA_MODE |
Data mode (live/cached/delayed) | live |
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
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.
Building a client without a token (no explicit apiKey(), no env var, no
.env entry) puts the client in demo mode: the Authorization header
is omitted from outbound requests and the SDK logs a warning at INFO
level. Authenticated endpoints will fail. Use this for read-only, public
endpoints only.
All SDK errors extend the sealed MarketDataException
hierarchy and carry support context (requestId, requestUrl,
statusCode, timestamp) plus a getSupportInfo() helper for support
tickets:
try (var client = new MarketDataClient()) {
var resp = client.stocks().quote(StockQuotesRequest.of("AAPL"));
System.out.println(resp.values());
} catch (RateLimitError e) {
System.err.println(e.getSupportInfo());
}The seven permitted subtypes are AuthenticationError, BadRequestError,
NotFoundError, RateLimitError, ServerError, NetworkError, and
ParseError. The hierarchy is sealed so switch over the subtypes is
compile-time exhaustive.
The repo uses Gradle (Kotlin DSL) with a version catalog at
gradle/libs.versions.toml.
The Gradle wrapper is committed (gradlew, gradlew.bat,
gradle/wrapper/gradle-wrapper.jar, gradle/wrapper/gradle-wrapper.properties),
so any JDK 17+ environment can build the project without a separate Gradle
install — the wrapper downloads the right Gradle version on first run.
./gradlew build # compile + unit tests + spotless + jacoco
./gradlew test # unit tests only
./gradlew spotlessApply # auto-format
./gradlew jacocoTestReport # coverage report → build/reports/jacoco/
# Integration tests hit the live API — gated by env var.
MARKETDATA_RUN_INTEGRATION_TESTS=true ./gradlew integrationTestEvery pull request runs the integration tests against the live API on
JDK 17, and their result is part of the required Tests passed check.
Two kinds of pull request skip them: one that only changes documentation,
and one from a fork or from dependabot, which cannot read the API token.
A reviewer with write access can also run them by posting a
slash-command on the first line of a PR comment — for the full JDK
matrix, for a fork or dependabot pull request, or to run them again
without a new push:
/integrationtest— JDK 17 only./integrationtestfull— full matrix{17, 21, 25}.
The first-line rule means quoted replies (> /integrationtest) and
prose that merely mentions the command do not fire a run. Anything that
isn't an exact match is rejected before any request is made.
com.marketdata.sdk # MarketDataClient, RateLimits, the resource façades
# (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
Every public package is @NullMarked (JSpecify): non-null is the default;
nullable items are tagged explicitly. This is what makes Kotlin's null
safety work against this Java API.
MIT.