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
107 changes: 107 additions & 0 deletions .sdk-sync/1.4.0-f19c2d4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# 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-16 12:44:32Z |
| Outcome | **done** |

## Changes in the API surface

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

## 1. tests

Coder (26 turns, US$ 0.62):

> Only the test file was touched, as required.
>
> Summary: Added tests in `OptionsResourceTest.java` specifying the new `dte` range behavior for `GET /v1/options/chain/{underlying}/` now that `?dte=` is a string rather than a plain integer:
>
> - `chainExpirationFilterDteRangeTranslatesToDashedDteParam` — expects a new `ExpirationFilter.dteRange(min, max)` factory to render the wire form `?dte=MIN-MAX`, mirroring the existing `StrikeFilter.range` "MIN-MAX" convention already used for the sibling `strike` parameter.
> - `expirationFilterDteRangeRejectsNegativeMin` — expects the same non-negative validation message as the existing single-value `ExpirationFilter.dte(int)`.
> - `expirationFilterDteRangeRejectsMinGreaterThanMax` — expects a `min must be <= max` validation error, matching `StrikeFilter.range`'s existing message convention.
>
> Ran `gradle test --no-daemon --console=plain`: it fails at `compileTestJava` with "cannot find symbol: method dteRange(int,int)" on `ExpirationFilter`, confirming the tests correctly specify not-yet-implemented behavior. No production code was modified.

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 (12 turns, US$ 0.24):

> All changes stay within the allowed paths (`src/main/java`, `src/test/java`, `docs`), and no `build.gradle*`/`settings.gradle*`/CI/gradle-wrapper files were touched. Nothing was committed.
>
> Summary for the PR:
>
> - Widened the options chain `dte` filter to match the API's `1.4.0` change of `?dte` from `integer` to `string`: added `ExpirationFilter.dteRange(int min, int max)`, a new sealed variant (`ExpirationFilter.DteRange`) that renders as `?dte=MIN-MAX`, mirroring the existing `StrikeFilter.range` dashed-string convention already used for `?strike=`.
> - Wired the new variant into `OptionsResource.applyExpirationFilter` alongside the existing `Dte`/`Between`/`MonthYear` handling; the pre-existing single-value `ExpirationFilter.dte(int)` (`?dte=N`) is unchanged.
> - Validation on `dteRange` mirrors the sibling factories: non-negative `min` (same message as `dte(int)`) and `min <= max` (same message as `StrikeFilter.range`).
> - Updated `docs/options/chain.md` to list the new `dteRange` filter alongside the other `ExpirationFilter` variants.
> - No test changes were needed beyond what was written in phase 1 — all phase-1 tests pass unmodified; `gradle test` and `gradle build` (including Spotless and JaCoCo) both succeed.

Gates:

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

## 3. review 1

Reviewer (2 turns, US$ 0.32): approved

> The change correctly implements the API delta: `?dte` widened from a bare integer to a string, enabling a min-max range. `ExpirationFilter.DteRange` is added as a new sealed variant with validation (non-negative min, min ≤ max) consistent with the existing `Dte`/`Between` factory-validation pattern, wired into `OptionsResource.applyExpirationFilter` to produce the dashed `dte=MIN-MAX` wire form mirroring `StrikeFilter.range`'s established convention. Tests cover the happy path (dashed wire format) and both validation failure modes, and the docs snippet is updated in the right place. The if-chain in `applyExpirationFilter` still isn't a compiler-enforced exhaustive switch, but that pattern predates this diff and isn't worsened by it. No missing cases, weakened tests, or public-API convention violations were found.

- no findings

Security (2 turns, US$ 0.23): approved

> The change adds an ExpirationFilter.dteRange(min, max) variant to support the API's dte parameter widening from int to string, plus corresponding wiring in OptionsResource.applyExpirationFilter and tests/docs. Inputs are validated ints (non-negative, min<=max) and rendered via plain string concatenation (`min + "-" + max`) into a query builder that presumably URL-encodes values consistently with existing sibling code (e.g., StrikeFilter.range already does the same pattern). There is no user-controlled string interpolation, no new I/O, no credential handling, and no new dependencies. No security issues found in the reviewed files.

- no findings

## 4. assess

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

> The public surface gains one new sealed-interface variant on com.marketdata.sdk.options.ExpirationFilter: the record ExpirationFilter.DteRange(int min, int max) and its static factory method ExpirationFilter.dteRange(int, int), which validate min >= 0 and min <= max before constructing. Because ExpirationFilter is a sealed interface, adding DteRange to its permits clause also alters the closed set of types consumers may switch/pattern-match over — existing exhaustive switches over ExpirationFilter that lack a default branch will fail to compile until updated to handle the new variant. No existing member (ExpirationFilter.dte(int), Dte, OnDate, Between, MonthYear, All, or any OptionsResource method signature) was removed or altered; the OptionsResource.java edit that maps DteRange to the "MIN-MAX" query string is internal query-building logic, not itself a public-surface change.

- changed: `ExpirationFilter.permits` — Sealed permits list gains ExpirationFilter.DteRange alongside OnDate, Dte, Between, MonthYear, All. Any consumer code that pattern-matches/switches exhaustively over ExpirationFilter without a `default` branch will no longer compile until a DteRange case is added.
- added: `ExpirationFilter.dteRange(int, int)` — New static factory returning ExpirationFilter.DteRange; throws IllegalArgumentException if min < 0 ("dte must be non-negative") or min > max ("min must be <= max").
- added: `ExpirationFilter.DteRange` — New public record variant of ExpirationFilter with components `int min()` and `int max()` and a public canonical constructor DteRange(int min, int max); serializes to the wire form ?dte=MIN-MAX via OptionsResource's internal query-building (package-private, not itself public surface).

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 documenting the new `ExpirationFilter.dteRange`/`DteRange` API (responding to Market Data API 1.4.0's `dte` int→string change) under `### Added`, plus a `### Changed` **BREAKING** note explaining the new sealed permit breaks non-`default` exhaustive switches over `ExpirationFilter` and how to migrate.

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: 48
- Tokens: 2,539,208
- Estimated cost: US$ 1.60

### Agent runs

- coder · claude-haiku-4-5-20251001, claude-sonnet-5 · 1,251,821 tokens (57,249 in, 1,179,388 cached, 15,184 out) · 26 turns · US$ 0.62
- coder · claude-sonnet-5 · 881,153 tokens (8,074 in, 869,880 cached, 3,199 out) · 12 turns · US$ 0.24
- reviewer · claude-haiku-4-5-20251001, claude-sonnet-5 · 62,802 tokens (56,060 in, 0 cached, 6,742 out) · 2 turns · US$ 0.32
- security · claude-haiku-4-5-20251001, claude-sonnet-5 · 56,482 tokens (49,514 in, 6,534 cached, 434 out) · 2 turns · US$ 0.23
- analyst · claude-haiku-4-5-20251001, claude-sonnet-5 · 49,146 tokens (24,494 in, 22,451 cached, 2,201 out) · 3 turns · US$ 0.13
- coder · claude-sonnet-5 · 237,804 tokens (3,536 in, 233,478 cached, 790 out) · 3 turns · US$ 0.07
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **`ExpirationFilter.dteRange(int min, int max)`** — days-to-expiration range filter for
`options().chain(...)`, rendering as `?dte=MIN-MAX`. Responds to Market Data API 1.4.0, which
widened `GET /v1/options/chain/{underlying}/`'s `dte` query parameter from an integer to a
string to allow a range alongside the existing single-value form. Throws
`IllegalArgumentException` if `min` is negative or `min > max`.

### Changed

- **BREAKING:** `ExpirationFilter` gains a new sealed permitted subtype, `DteRange`, alongside
`OnDate`, `Dte`, `Between`, `MonthYear`, and `All`. Consumer code that pattern-matches or
`switch`es exhaustively over `ExpirationFilter` without a `default` branch will no longer
compile. **Migration:** add a `case ExpirationFilter.DteRange dr -> ...` arm (or a `default`
branch) to any exhaustive `switch` over `ExpirationFilter`.

## [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 min, int max)) // days-to-expiration 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.min() + "-" + v.max());
} 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}, mirroring {@link
* StrikeFilter#range}'s dashed-string convention. {@code min} must not exceed {@code max}.
*/
static DteRange dteRange(int min, int max) {
if (min < 0) {
throw new IllegalArgumentException("dte must be non-negative");
}
if (min > max) {
throw new IllegalArgumentException("min must be <= max");
}
return new DteRange(min, max);
}

/**
* 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 min, int max) 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() {
// v1.4.0 widened ?dte= from a bare integer to a string so a min-max range can be expressed,
// mirroring the existing StrikeFilter.range "MIN-MAX" wire convention.
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 expirationFilterDteRangeRejectsNegativeMin() {
assertThatThrownBy(() -> ExpirationFilter.dteRange(-1, 45))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("dte must be non-negative");
}

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

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