Skip to content
Closed
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
101 changes: 101 additions & 0 deletions .sdk-sync/1.4.0-f19c2d4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# sdk-auto-sync job log

| | |
|---|---|
| API release | 1.4.0 (commit `f19c2d4`) |
| SDK | sdk-java |
| Branch | `sdk-sync/1.4.0-f19c2d4` |
| API surface | `9c18fbb1f58e` → `72b3d15f59ad` |
| Date | 2026-09-15 18:45:04Z |
| Outcome | **done** |

## Changes in the API surface

~ GET /v1/options/chain/{underlying}/ ?dte: string (was integer)

## 1. tests

Coder (23 turns, US$ 0.55):

> The build fails as required, with a compilation error on the not-yet-implemented `ExpirationFilter.dteRange(int, int)` symbol — confirming the tests are correctly specifying new behaviour ahead of implementation.
>
> ## Summary
>
> Added three unit tests to `src/test/java/com/marketdata/sdk/OptionsResourceTest.java` specifying the new behaviour for the `dte` query parameter's `integer` → `string` type change on `GET /v1/options/chain/{underlying}/`, mirroring the existing `?strike=` range convention (`StrikeFilter.range`):
>
> - `chainExpirationFilterDteRangeTranslatesToDashedDteParam` — `ExpirationFilter.dteRange(30, 45)` must translate to `?dte=30-45` on the wire.
> - `expirationFilterDteRangeRejectsMinGreaterThanMax` — `dteRange(45, 30)` must throw `IllegalArgumentException` ("min must be <= max").
> - `expirationFilterDteRangeRejectsNegativeMin` — `dteRange(-1, 30)` must throw `IllegalArgumentException` ("dte must be non-negative").
>
> The existing `dte(int)` exact-value tests and validation were left untouched. Ran `gradle test --no-daemon --console=plain`; it fails at `:compileTestJava` because `ExpirationFilter.dteRange(int, int)` doesn't exist yet, confirming the tests correctly drive the required implementation.

Gates:

- tests (expected to fail), attempt 1: failed — Execution failed for task ':compileTestJava' (registered by plugin class 'org.gradle.api.plugins.JavaBasePlugin').

## 2. implement

Coder (11 turns, US$ 0.21):

> All changes are within the allowed paths (`src/main/java`, `src/test/java`, `docs/`), and I did not touch the tests written in phase 1 beyond what already existed.
>
> ## Summary
>
> Implemented the `1.4.0` API change to `GET /v1/options/chain/{underlying}/`'s `dte` query parameter (integer → string), following the same sealed-filter convention `StrikeFilter` already uses for its `?strike=` range syntax:
>
> - `ExpirationFilter`: added a new sealed variant `DteRange(minDays, maxDays)` with factory `ExpirationFilter.dteRange(int, int)`, validating `minDays >= 0` and `minDays <= maxDays` (mirrors `dte(int)`'s and `StrikeFilter.range`'s existing validation messages).
> - `OptionsResource.applyExpirationFilter`: renders `DteRange` as `?dte=min-max`, keeping the exhaustive if-chain over the now six-variant sealed type complete (no silent no-op for the new case).
> - `docs/options/chain.md`: documented the new `dteRange(...)` builder call alongside the existing `dte(...)`.
>
> No existing tests were weakened — the phase-1 tests are unchanged and now compile and pass. `gradle test` and `gradle build` (including Spotless and JaCoCo) both succeed.

Gates:

- tests, attempt 1: passed
- build, attempt 1: passed

## 3. review 1

Reviewer (14 turns, US$ 0.28): approved

> The change correctly implements the API's widening of the options-chain `?dte=` parameter from integer to string by adding a new sealed ExpirationFilter.DteRange variant alongside the existing Dte, serialized as "min-max" on the wire — closely mirroring the established StrikeFilter.Range convention (including matching validation and error messages). The existing single-value `dte(int)` path is untouched and still correct. Tests cover the new URL translation and both validation failure modes (min>max, negative min), matching the depth of coverage given to sibling filter variants in the same test file. Docs (docs/options/chain.md) were updated consistently. No public signatures were broken, no unrelated files were touched, and no existing tests were weakened.

- no findings

Security (7 turns, US$ 0.12): approved

> The change adds ExpirationFilter.dteRange(minDays, maxDays) to support the API's dte int→string widening, formatting the query value as "min-max" from two validated int parameters. Since inputs are constrained to int (not free-form strings), there is no injection risk, and the value still passes through the transport's existing URL-encoding path. Validation (non-negative, min<=max) mirrors the existing Dte/StrikeFilter.range patterns, and tests cover both the happy path and validation errors. No secrets, unsafe defaults, new dependencies, or out-of-scope changes were introduced.

- no findings

## 4. assess

Analyst (3 turns, US$ 0.11) — public surface:

> This change adds a new days-to-expiration range option to the options chain request builder, mirroring the existing strike-range syntax. The sealed ExpirationFilter interface gains a new permitted variant, DteRange, plus a static factory method dteRange(int, int), and its permits list is updated accordingly. OptionsResource is updated internally to translate the new variant to the dte=min-max query wire form, but this is not a public-surface change on its own since OptionsResource's chain-building logic is package-internal branching, not a new/changed public method. No existing public member (type, method, field, or permits entry) was removed or had its signature/behavior changed; all existing ExpirationFilter factory methods and record types are untouched.

- added: `ExpirationFilter.dteRange(int, int)` — New static factory method returning a DteRange filter; throws IllegalArgumentException if minDays is negative or minDays > maxDays. Consumers can now call this method that did not exist before.
- added: `ExpirationFilter.DteRange` — New public record type (minDays, maxDays) implementing the sealed ExpirationFilter interface; serializes to the chain endpoint's ?dte=min-max wire form.
- changed: `ExpirationFilter.permits` — The sealed interface's permits list gains ExpirationFilter.DteRange alongside the existing OnDate, Dte, Between, MonthYear, All. Any consumer exhaustive switch over ExpirationFilter without a default branch will no longer compile until a DteRange case is added — a source-compatibility break for that specific pattern, though additive in intent.

Computed by the pipeline: **major**, 1.0.0 → 2.0.0; changelog entry written

## 5. changelog

Coder (3 turns, US$ 0.07):

> Added an `## [Unreleased]` entry noting it responds to API 1.4.0, with an `### Added` line for `ExpirationFilter.dteRange(...)` and a `### Changed` **BREAKING** line explaining the sealed-interface `permits` expansion and how to migrate exhaustive switches.

Gates:

- build, attempt 1: passed

## Totals

- Version: major · 1.0.0 → 2.0.0
- Coder runs: 3
- Review runs: 2
- Gates run: 4 (1 failed)
- Files changed by the agent: 5
- Turns: 61
- Estimated cost: US$ 1.34
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

Responds to Market Data API release 1.4.0, which widened the options chain
endpoint's `?dte=` query parameter from an integer to a string so it can carry
a range in addition to a single value.

### Added

- **`ExpirationFilter.dteRange(int minDays, int maxDays)`** — new
days-to-expiration range filter for `client.options().chain(...)`, wire form
`?dte=min-max`. Throws `IllegalArgumentException` if `minDays` is negative or
greater than `maxDays`.

### Changed

- **BREAKING:** `ExpirationFilter`'s sealed `permits` list gains
`ExpirationFilter.DteRange` alongside `OnDate`, `Dte`, `Between`,
`MonthYear`, and `All`. Any consumer with an exhaustive `switch` over
`ExpirationFilter` and no `default` branch will fail to compile until it
adds a `DteRange` case. To migrate, add a case for
`ExpirationFilter.DteRange` (or a `default` branch) to any such `switch`.

## [1.0.0] - 2026-06-29

First stable release of the Market Data Java &amp; Kotlin SDK — a single JVM
Expand Down
1 change: 1 addition & 0 deletions docs/options/chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ OptionsChainRequest.builder(String symbol)
// Expiration selection (sealed ExpirationFilter — pick one):
.expirationFilter(ExpirationFilter.onDate(LocalDate date))
.expirationFilter(ExpirationFilter.dte(int days)) // days-to-expiration
.expirationFilter(ExpirationFilter.dteRange(int minDays, int maxDays)) // dte range
.expirationFilter(ExpirationFilter.between(LocalDate from, LocalDate to))
.expirationFilter(ExpirationFilter.all()) // every expiration
// (if omitted, the API narrows to the front month)
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/marketdata/sdk/OptionsResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ private static void applyExpirationFilter(RequestSpec.Builder b, ExpirationFilte
b.query("expiration", DateTimeFormatter.ISO_LOCAL_DATE.format(v.date()));
} else if (f instanceof ExpirationFilter.Dte v) {
b.query("dte", v.days());
} else if (f instanceof ExpirationFilter.DteRange v) {
b.query("dte", v.minDays() + "-" + v.maxDays());
} else if (f instanceof ExpirationFilter.Between v) {
b.query("from", DateTimeFormatter.ISO_LOCAL_DATE.format(v.from()));
b.query("to", DateTimeFormatter.ISO_LOCAL_DATE.format(v.to()));
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/marketdata/sdk/options/ExpirationFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
public sealed interface ExpirationFilter
permits ExpirationFilter.OnDate,
ExpirationFilter.Dte,
ExpirationFilter.DteRange,
ExpirationFilter.Between,
ExpirationFilter.MonthYear,
ExpirationFilter.All {
Expand Down Expand Up @@ -47,6 +48,20 @@ static Dte dte(int days) {
return new Dte(days);
}

/**
* Days-to-expiration range — wire form {@code ?dte=min-max}. {@code minDays} must not exceed
* {@code maxDays}.
*/
static DteRange dteRange(int minDays, int maxDays) {
if (minDays < 0) {
throw new IllegalArgumentException("dte must be non-negative");
}
if (minDays > maxDays) {
throw new IllegalArgumentException("min must be <= max");
}
return new DteRange(minDays, maxDays);
}

/**
* Inclusive date range — wire form {@code ?from=YYYY-MM-DD&to=YYYY-MM-DD}. {@code from} must not
* be strictly after {@code to}.
Expand Down Expand Up @@ -79,6 +94,8 @@ record OnDate(LocalDate date) implements ExpirationFilter {

record Dte(int days) implements ExpirationFilter {}

record DteRange(int minDays, int maxDays) implements ExpirationFilter {}

record Between(LocalDate from, LocalDate to) implements ExpirationFilter {}

record MonthYear(int year, int month) implements ExpirationFilter {}
Expand Down
32 changes: 32 additions & 0 deletions src/test/java/com/marketdata/sdk/OptionsResourceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,24 @@ void chainExpirationFilterDteTranslatesToDteParam() {
.isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=30");
}

@Test
void chainExpirationFilterDteRangeTranslatesToDashedDteParam() {
// The chain endpoint's ?dte= param widened from integer to string so it can express a range,
// mirroring the existing ?strike= range syntax (StrikeFilter.range -> "min-max").
CapturingClient client = okWith(CANNED_CHAIN_BODY);
OptionsResource options = resourceWith(client);

options
.chainAsync(
OptionsChainRequest.builder("AAPL")
.expirationFilter(ExpirationFilter.dteRange(30, 45))
.build())
.join();

assertThat(client.captured.get(0).uri().toString())
.isEqualTo("http://localhost/v1/options/chain/AAPL/?dte=30-45");
}

@Test
void chainExpirationFilterBetweenTranslatesToFromTo() {
CapturingClient client = okWith(CANNED_CHAIN_BODY);
Expand Down Expand Up @@ -1203,6 +1221,20 @@ void expirationFilterDteRejectsNegative() {
.hasMessageContaining("dte must be non-negative");
}

@Test
void expirationFilterDteRangeRejectsMinGreaterThanMax() {
assertThatThrownBy(() -> ExpirationFilter.dteRange(45, 30))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("min must be <= max");
}

@Test
void expirationFilterDteRangeRejectsNegativeMin() {
assertThatThrownBy(() -> ExpirationFilter.dteRange(-1, 30))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("dte must be non-negative");
}

@Test
void expirationFilterBetweenRejectsReversedDates() {
assertThatThrownBy(
Expand Down
Loading