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

## Changes in the API surface

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

## 1. tests

Coder (22 turns, US$ 0.42):

> Only the test file was touched, as required.
>
> ## Summary
>
> The API delta changes `?dte=` on `GET /v1/options/chain/{underlying}/` from `integer` to `string`, mirroring the existing `?strike=` parameter which already accepts a dashed range as a string (`StrikeFilter.range`). I added three tests to `src/test/java/com/marketdata/sdk/OptionsResourceTest.java` that specify the new `ExpirationFilter.dte(int minDays, int maxDays)` range overload, alongside the existing single-value `dte(int)`:
>
> - `chainExpirationFilterDteRangeTranslatesToDashedDteParam` — asserts `ExpirationFilter.dte(30, 60)` serializes the chain request query string as `?dte=30-60`.
> - `expirationFilterDteRangeRejectsNegativeMin` — asserts `dte(-1, 30)` throws `IllegalArgumentException` ("dte must be non-negative").
> - `expirationFilterDteRangeRejectsMinGreaterThanMax` — asserts `dte(60, 30)` throws `IllegalArgumentException` ("min must be <= max").
>
> Confirmed `gradle test --no-daemon --console=plain` fails at compilation (the two-arg `dte` overload doesn't exist yet), as expected for this tests-only phase. No production code or docs were touched.

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 (15 turns, US$ 0.28):

> All changes fall within `src/main/java/**`, `src/test/java/**`, and `docs/**`, as required. No commits made.
>
> ## Summary
>
> Implemented `?dte=` widening to `string` on `GET /v1/options/chain/{underlying}/` by adding a days-to-expiration **range** alongside the existing single-value filter:
>
> - `ExpirationFilter.DteRange` — new sealed-interface variant (`minDays`, `maxDays`), added to the `permits` list.
> - `ExpirationFilter.dte(int minDays, int maxDays)` — new overload of the existing `dte` factory, validating non-negativity and `min <= max` (mirrors `StrikeFilter.range`'s validation style).
> - `OptionsResource.applyExpirationFilter` — serializes `DteRange` as `?dte=min-max` (e.g. `dte=30-60`), matching the dashed-range convention already used for `?strike=`.
> - `docs/options/chain.md` — added the new `dte(minDays, maxDays)` builder call to the request-shape listing.
>
> Fixed one Spotless formatting violation in the phase-1 test (line wrap on the range test) — no assertions were weakened. `gradle test` and `gradle build` both pass.

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's `dte` query-parameter type widening (int → string) by adding a new `ExpirationFilter.DteRange` sealed variant and a `dte(int minDays, int maxDays)` factory, rendering it as the dashed `dte=min-max` wire form. This mirrors the existing `StrikeFilter.range` convention already used in the same file, keeps the sealed interface's `permits` list, javadoc, and validation style consistent with sibling variants (`Dte`, `Between`), updates `OptionsResource.applyExpirationFilter` correctly, and updates `docs/options/chain.md` in the right place. Validation (non-negative bounds, min ≤ max) and error messages match sibling conventions, and new tests cover the happy path and both major validation failure modes. No public-API, convention, or test-weakening issues were found; the only gap is a minor missing test case for the negative-`maxDays` validation branch."

- [minor] src/test/java/com/marketdata/sdk/OptionsResourceTest.java — The new `expirationFilterDteRangeRejectsNegativeMin` test only exercises the `minDays < 0` branch of `ExpirationFilter.dte(int, int)`'s validation. There is no corresponding test for a negative `maxDays` with a non-negative `minDays` (e.g. `dte(5, -1)`), leaving that branch of the `minDays < 0 || maxDays < 0` condition unverified.

Security (4 turns, US$ 0.25): approved

> This change adds ExpirationFilter.dte(int minDays, int maxDays), a new sealed-interface variant producing the wire form ?dte=min-max, matching the API's dte parameter widening from integer to string. Both minDays and maxDays are primitive ints validated non-negative and min<=max before being concatenated into the query value, so there is no injection surface (no free-form string input reaches the query string). The change is narrowly scoped to the stated API update, is fully covered by new unit tests (encoding and validation-rejection paths), and the docs update accurately reflects the new builder method. No secrets, unsafe defaults, altered dependencies, or unrelated modifications were introduced.

- no findings

## 4. assess

Analyst (5 turns, US$ 0.09) — public surface:

> The diff adds one new public surface member (the ExpirationFilter.dte(int minDays, int maxDays) static factory and its backing ExpirationFilter.DteRange record with minDays()/maxDays() accessors) to represent the API's now-string-typed ?dte= range form, and widens the sealed ExpirationFilter interface's permits clause to include DteRange. The existing ExpirationFilter.dte(int days)/Dte record, and all other ExpirationFilter variants, are unchanged in signature. The only surface-breaking effect is on the sealed hierarchy itself: because permits gained a new case, any consumer with an exhaustive switch over ExpirationFilter no longer compiles until a DteRange arm is added — the internal OptionsResource query-translation logic that consumes the new variant is a private helper and not part of the public surface.

- added: `ExpirationFilter.dte(int, int)` — New static factory overload returning ExpirationFilter.DteRange; validates minDays/maxDays non-negative and minDays <= maxDays, translates to wire form ?dte=min-max. Distinct arity from the existing dte(int) overload, so no overload-resolution conflict for existing callers.
- added: `ExpirationFilter.DteRange` — New public record type implementing ExpirationFilter, with accessors minDays() and maxDays(); added as a new permitted subtype of the sealed interface.
- changed: `ExpirationFilter.permits` — Sealed interface's permits list now includes ExpirationFilter.DteRange alongside OnDate, Dte, Between, MonthYear, All. Any consumer code that exhaustively switches over ExpirationFilter (relying on compiler-enforced exhaustiveness) will fail to compile until it adds a DteRange case.

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

## 5. changelog

Coder (3 turns, US$ 0.06):

> Added a `### Changed` entry under `[Unreleased]` documenting the new `ExpirationFilter.dte(int, int)`/`DteRange` range filter (API 1.4.0's `dte` param widening to a string), flagged BREAKING with migration guidance for exhaustive `switch` consumers.

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: 51
- Tokens: 2,398,386
- Estimated cost: US$ 1.43

### Agent runs

- coder · claude-haiku-4-5-20251001, claude-sonnet-5 · 950,333 tokens (939,677 in, 10,656 out) · 22 turns · US$ 0.42
- coder · claude-sonnet-5 · 974,788 tokens (970,820 in, 3,968 out) · 15 turns · US$ 0.28
- reviewer · claude-haiku-4-5-20251001, claude-sonnet-5 · 64,892 tokens (56,064 in, 8,828 out) · 2 turns · US$ 0.32
- security · claude-haiku-4-5-20251001, claude-sonnet-5 · 113,651 tokens (112,635 in, 1,016 out) · 4 turns · US$ 0.25
- analyst · claude-haiku-4-5-20251001, claude-sonnet-5 · 76,526 tokens (74,375 in, 2,151 out) · 5 turns · US$ 0.09
- coder · claude-sonnet-5 · 218,196 tokens (217,473 in, 723 out) · 3 turns · US$ 0.06
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **BREAKING:** `ExpirationFilter.dte` now also accepts a days-to-expiration
range (`ExpirationFilter.dte(minDays, maxDays)`), translating to the wire
form `?dte=min-max`, in response to the Market Data API 1.4.0 change
widening the options chain `dte` query parameter from an integer to a
string. This adds `ExpirationFilter.DteRange` as a new permitted subtype of
the sealed `ExpirationFilter` interface, so any code that exhaustively
`switch`es over `ExpirationFilter` will fail to compile until it adds a
`DteRange` case — add a branch (e.g. `case ExpirationFilter.DteRange r -> …`)
alongside the existing `OnDate` / `Dte` / `Between` / `MonthYear` / `All`
cases. The existing single-value `ExpirationFilter.dte(int)` overload and
its `?dte=N` wire form are unchanged.

## [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.dte(int minDays, int maxDays)) // 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.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 dte(int minDays, int maxDays) {
if (minDays < 0 || maxDays < 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() {
// §11.4-delta: ?dte= now accepts a string, widening the single day count to a "min-max" range
// (mirrors StrikeFilter.range's dashed wire form for ?strike=).
CapturingClient client = okWith(CANNED_CHAIN_BODY);
OptionsResource options = resourceWith(client);

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

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

@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.dte(-1, 30))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("dte must be non-negative");
}

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

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