diff --git a/.sdk-sync/1.4.0-f19c2d4.md b/.sdk-sync/1.4.0-f19c2d4.md new file mode 100644 index 0000000..c0c51c1 --- /dev/null +++ b/.sdk-sync/1.4.0-f19c2d4.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 53c39be..57b3403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 & Kotlin SDK — a single JVM diff --git a/docs/options/chain.md b/docs/options/chain.md index d71c38b..0794778 100644 --- a/docs/options/chain.md +++ b/docs/options/chain.md @@ -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) diff --git a/src/main/java/com/marketdata/sdk/OptionsResource.java b/src/main/java/com/marketdata/sdk/OptionsResource.java index 81fc7ee..00ec485 100644 --- a/src/main/java/com/marketdata/sdk/OptionsResource.java +++ b/src/main/java/com/marketdata/sdk/OptionsResource.java @@ -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())); diff --git a/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java b/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java index 3b05379..ee01d39 100644 --- a/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java +++ b/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java @@ -18,6 +18,7 @@ public sealed interface ExpirationFilter permits ExpirationFilter.OnDate, ExpirationFilter.Dte, + ExpirationFilter.DteRange, ExpirationFilter.Between, ExpirationFilter.MonthYear, ExpirationFilter.All { @@ -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}. @@ -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 {} diff --git a/src/test/java/com/marketdata/sdk/OptionsResourceTest.java b/src/test/java/com/marketdata/sdk/OptionsResourceTest.java index 292ee83..84d93b1 100644 --- a/src/test/java/com/marketdata/sdk/OptionsResourceTest.java +++ b/src/test/java/com/marketdata/sdk/OptionsResourceTest.java @@ -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); @@ -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(