diff --git a/.sdk-sync/1.4.0-f19c2d4.md b/.sdk-sync/1.4.0-f19c2d4.md new file mode 100644 index 0000000..2918faf --- /dev/null +++ b/.sdk-sync/1.4.0-f19c2d4.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 53c39be..82d9f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 & Kotlin SDK — a single JVM diff --git a/docs/options/chain.md b/docs/options/chain.md index d71c38b..54eb442 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.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) 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..b8653b0 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 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}. @@ -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..4900f6b 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() { + // §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); @@ -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(