diff --git a/.sdk-sync/1.4.0-f19c2d4.md b/.sdk-sync/1.4.0-f19c2d4.md new file mode 100644 index 0000000..71d3932 --- /dev/null +++ b/.sdk-sync/1.4.0-f19c2d4.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 53c39be..52afa1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 & Kotlin SDK — a single JVM diff --git a/docs/options/chain.md b/docs/options/chain.md index d71c38b..a475d37 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 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) diff --git a/src/main/java/com/marketdata/sdk/OptionsResource.java b/src/main/java/com/marketdata/sdk/OptionsResource.java index 81fc7ee..802eb89 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.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())); diff --git a/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java b/src/main/java/com/marketdata/sdk/options/ExpirationFilter.java index 3b05379..bcfeee8 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}, 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}. @@ -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 {} diff --git a/src/test/java/com/marketdata/sdk/OptionsResourceTest.java b/src/test/java/com/marketdata/sdk/OptionsResourceTest.java index 292ee83..31b278f 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() { + // 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); @@ -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(